/*-------------------------------------------------------------------------
 *
 * costsize.c
 *	  计算（和设置）关系大小和路径成本的例程
 *
 * 路径成本以这些基本参数建立的任意单位进行测量：
 *
 *	seq_page_cost		顺序页面提取的成本
 *	random_page_cost	非顺序页面提取的成本
 *	cpu_tuple_cost		处理元组的典型CPU时间成本
 *	cpu_index_tuple_cost  处理索引元组的典型CPU时间成本
 *	cpu_operator_cost	执行运算符或函数的CPU时间成本
 *	parallel_tuple_cost 从工作线程到主后端传递元组的CPU时间成本
 *	parallel_setup_cost 为并行设置共享内存的成本
 *
 * 我们预计内核通常会进行一定程度的预读优化；这与寻道成本结合意味着
 * seq_page_cost 通常明显低于 random_page_cost。 （但是，如果数据库完全缓存到RAM中，
 * 那么设置它们相等是合理的。）
 *
 * 我们还使用一个粗略的估计"effective_cache_size"来表示Postgres+操作系统级别
 * 磁盘缓存中的磁盘页面数量。 （我们不能仅仅使用 NBuffers 来实现这一目的，
 * 因为那样会忽略内核磁盘缓存的影响。）
 *
 * 显然，对这些值的取常数是一种过于简化，但即使在这个细节水平上获得任何有用的估计也是相当困难的。
 * 请注意，这些参数都是用户可设置的，以防默认值对特定平台大相径庭。
 *
 * seq_page_cost 和 random_page_cost 还可以被单独的表空间覆盖，以防某些数据位于快磁盘而其他数据位于慢磁盘。
 * 每个表空间的覆盖从不适用于诸如外部排序或溢出work_mem的物化节点等临时工作文件。
 *
 * 我们为每个路径计算两个不同的成本：
 *		total_cost: 获取所有元组的总估计成本
 *		startup_cost: 在获取第一个元组之前产生的成本
 * 在某些场景下，例如当存在LIMIT或我们正在实现 EXISTS(...) 子查询时，
 * 并不需要获取路径结果的所有元组。调用者可以通过在 startup_cost 和 total_cost 之间进行插值来估算
 * 获取部分结果的成本。具体来说：
 *		actual_cost = startup_cost +
 *			(total_cost - startup_cost) * tuples_to_fetch / path->rows;
 * 请注意，基础关系的行数（以及根据扩展，LIMIT节点下的plan_rows）是在不考虑任何LIMIT的情况下设置的，
 * 以便这个方程式能够正确工作。 （注意：虽然普通关系的path->rows永远不会为零，
 * 但对于可证明为空关系的路径，它的行数为零，因此要注意零除。）LIMIT被应用为顶层计划节点。
 *
 * 出于很大程度上的历史原因，此模块中的大多数例程仅使用传递的结果Path来存储其结果
 * （rows、startup_cost和total_cost）。所有它们所需的输入数据都作为单独的参数传递，
 * 尽管其中许多数据可以从Path中提取。
 * 对cost_XXXjoin()例程做了例外，期望传入的XXXPath的所有其他字段都填充，
 * 同样，cost_index()假定传入的IndexPath是有效的，除其输出值外。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/optimizer/path/costsize.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <limits.h>
#include <math.h>

#include "access/amapi.h"
#include "access/htup_details.h"
#include "access/tsmapi.h"
#include "executor/executor.h"
#include "executor/nodeAgg.h"
#include "executor/nodeHash.h"
#include "executor/nodeMemoize.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/optimizer.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "optimizer/placeholder.h"
#include "optimizer/plancat.h"
#include "optimizer/planmain.h"
#include "optimizer/restrictinfo.h"
#include "parser/parsetree.h"
#include "utils/lsyscache.h"
#include "utils/selfuncs.h"
#include "utils/spccache.h"
#include "utils/tuplesort.h"


#define LOG2(x)  (log(x) / 0.693147180559945)

/*
 * 附加和合并节点的成本比使用cpu_tuple_cost的其他操作要低；
 * 因此不再增加单独的GUC，将每个元组的成本估算为cpu_tuple_cost乘以该值。
 */
#define APPEND_CPU_COST_MULTIPLIER 0.5

/*
 * 行估计的最大值。我们将行估计限制在此，以帮助确保
 * 基于这些估计的成本保持在double可以表示的范围内。
 * add_path()在成本值为无限或NaN时不会正常工作。
 */
#define MAXIMUM_ROWCOUNT 1e100

double		seq_page_cost = DEFAULT_SEQ_PAGE_COST;
double		random_page_cost = DEFAULT_RANDOM_PAGE_COST;
double		cpu_tuple_cost = DEFAULT_CPU_TUPLE_COST;
double		cpu_index_tuple_cost = DEFAULT_CPU_INDEX_TUPLE_COST;
double		cpu_operator_cost = DEFAULT_CPU_OPERATOR_COST;
double		parallel_tuple_cost = DEFAULT_PARALLEL_TUPLE_COST;
double		parallel_setup_cost = DEFAULT_PARALLEL_SETUP_COST;
double		recursive_worktable_factor = DEFAULT_RECURSIVE_WORKTABLE_FACTOR;

int			effective_cache_size = DEFAULT_EFFECTIVE_CACHE_SIZE;

Cost		disable_cost = 1.0e10;

int			max_parallel_workers_per_gather = 2;

bool		enable_seqscan = true;
bool		enable_indexscan = true;
bool		enable_indexonlyscan = true;
bool		enable_bitmapscan = true;
bool		enable_tidscan = true;
bool		enable_sort = true;
bool		enable_incremental_sort = true;
bool		enable_hashagg = true;
bool		enable_nestloop = true;
bool		enable_material = true;
bool		enable_memoize = true;
bool		enable_mergejoin = true;
bool		enable_hashjoin = true;
bool		enable_gathermerge = true;
bool		enable_partitionwise_join = false;
bool		enable_partitionwise_aggregate = false;
bool		enable_parallel_append = true;
bool		enable_parallel_hash = true;
bool		enable_partition_pruning = true;
bool		enable_async_append = true;

typedef struct
{
	PlannerInfo *root;
	QualCost	total;
} cost_qual_eval_context;

static List *fc_extract_nonindex_conditions(List *fc_qual_clauses, List *fc_indexclauses);
static MergeScanSelCache *fc_cached_scansel(PlannerInfo *fc_root,
										 RestrictInfo *fc_rinfo,
										 PathKey *fc_pathkey);
static void fc_cost_rescan(PlannerInfo *fc_root, Path *fc_path,
						Cost *fc_rescan_startup_cost, Cost *fc_rescan_total_cost);
static bool fc_cost_qual_eval_walker(Node *fc_node, cost_qual_eval_context *fc_context);
static void get_restriction_qual_cost(PlannerInfo *fc_root, RelOptInfo *fc_baserel,
									  ParamPathInfo *fc_param_info,
									  QualCost *fc_qpqual_cost);
static bool fc_has_indexed_join_quals(NestPath *fc_path);
static double fc_approx_tuple_count(PlannerInfo *fc_root, JoinPath *fc_path,
								 List *fc_quals);
static double fc_calc_joinrel_size_estimate(PlannerInfo *fc_root,
										 RelOptInfo *fc_joinrel,
										 RelOptInfo *fc_outer_rel,
										 RelOptInfo *fc_inner_rel,
										 double fc_outer_rows,
										 double fc_inner_rows,
										 SpecialJoinInfo *fc_sjinfo,
										 List *fc_restrictlist);
static Selectivity get_foreign_key_join_selectivity(PlannerInfo *fc_root,
													Relids fc_outer_relids,
													Relids fc_inner_relids,
													SpecialJoinInfo *fc_sjinfo,
													List **fc_restrictlist);
static Cost fc_append_nonpartial_cost(List *fc_subpaths, int fc_numpaths,
								   int fc_parallel_workers);
static void set_rel_width(PlannerInfo *fc_root, RelOptInfo *fc_rel);
static double fc_relation_byte_size(double fc_tuples, int fc_width);
static double fc_page_size(double fc_tuples, int fc_width);
static double get_parallel_divisor(Path *fc_path);


/*
 * clamp_row_est
 *		将行数估计强制为一个合理的值。
 */
double clamp_row_est(double fc_nrows)
{
	/*
	 * 避免无限和NaN的行估计。从这些值衍生出的成本将是
	 * 无用的。同时将估计值强制设置为至少一行，以使解释输出
	 * 看起来更好，并避免在插值成本时可能出现的零除错误。
	 * 还要将其设为整数。
	 */
	if (fc_nrows > MAXIMUM_ROWCOUNT || isnan(fc_nrows))
		fc_nrows = MAXIMUM_ROWCOUNT;
	else if (fc_nrows <= 1.0)
		fc_nrows = 1.0;
	else
		fc_nrows = rint(fc_nrows);

	return fc_nrows;
}

/*
 * clamp_cardinality_to_long
 *		将Cardinality值转换为一个合理的长整型值。
 */
long clamp_cardinality_to_long(Cardinality fc_x)
{
	/*
	 * 仅出于防范的目的，确保我们对负值或
	 * NaN值做出合理的处理。
	 */
	if (isnan(fc_x))
		return LONG_MAX;
	if (fc_x <= 0)
		return 0;

	/*
	 * 如果“long”是64位的，则LONG_MAX无法被精确表示为
	 * double。将其转换为double再返回可能会由于四舍五入导致
	 * 溢出，因此避免这样做。我们相信任何严格小于
	 * "(double) LONG_MAX"的double值在转换为
	 * 可表示的“long”值时不会出现问题。
	 */
	return (fc_x < (double) LONG_MAX) ? (long) fc_x : LONG_MAX;
}


/*
 * cost_seqscan
 *	  确定并返回顺序扫描关系的成本。
 *
 * 'baserel'是要扫描的关系
 * 'param_info'是该路径为参数化路径时的ParamPathInfo，否则为NULL
 */
void cost_seqscan(Path *fc_path, PlannerInfo *fc_root,
			 RelOptInfo *fc_baserel, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_cpu_run_cost;
	Cost		fc_disk_run_cost;
	double		fc_spc_seq_page_cost;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;

	/* 应仅应用于基础关系 */
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_RELATION);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	if (!enable_seqscan)
		fc_startup_cost += disable_cost;

	/* 获取包含表格的表空间的估计页面成本 */
	get_tablespace_page_costs(fc_baserel->reltablespace,
							  NULL,
							  &fc_spc_seq_page_cost);

	/*
	 * 磁盘成本
	 */
	fc_disk_run_cost = fc_spc_seq_page_cost * fc_baserel->pages;

	/* CPU成本 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_cpu_run_cost = fc_cpu_per_tuple * fc_baserel->tuples;
	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->pathtarget->cost.startup;
	fc_cpu_run_cost += fc_path->pathtarget->cost.per_tuple * fc_path->rows;

	/* 如果使用并行性，则调整成本。 */
	if (fc_path->parallel_workers > 0)
	{
		double		fc_parallel_divisor = get_parallel_divisor(fc_path);

		/* CPU成本在所有工作者之间分摊。 */
		fc_cpu_run_cost /= fc_parallel_divisor;

		/*
		 * 可能可以摊销一些I/O成本，但可能
		 * 不会很多，因为大多数操作系统已经进行了 agresive
		 * 预取。目前，我们假设磁盘运行成本无法
		 * 完全摊销。
		 */

		/*
		 * 在并行计划的情况下，行计数需要代表
		 * 每个工作者处理的元组数量。
		 */
		fc_path->rows = clamp_row_est(fc_path->rows / fc_parallel_divisor);
	}

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_cpu_run_cost + fc_disk_run_cost;
}

/*
 * cost_samplescan
 *	  确定并返回使用采样扫描关系的成本。
 *
 * 'baserel'是要扫描的关系
 * 'param_info'是该路径为参数化路径时的ParamPathInfo，否则为NULL
 */
void cost_samplescan(Path *fc_path, PlannerInfo *fc_root,
				RelOptInfo *fc_baserel, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	RangeTblEntry *fc_rte;
	TableSampleClause *fc_tsc;
	TsmRoutine *fc_tsm;
	double		fc_spc_seq_page_cost,
				fc_spc_random_page_cost,
				fc_spc_page_cost;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;

	/* 应仅应用于具有tablesample子句的基础关系 */
	Assert(fc_baserel->relid > 0);
	fc_rte = planner_rt_fetch(fc_baserel->relid, fc_root);
	Assert(fc_rte->rtekind == RTE_RELATION);
	fc_tsc = fc_rte->tablesample;
	Assert(fc_tsc != NULL);
	fc_tsm = GetTsmRoutine(fc_tsc->tsmhandler);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	/* 获取包含表格的表空间的估计页面成本 */
	get_tablespace_page_costs(fc_baserel->reltablespace,
							  &fc_spc_random_page_cost,
							  &fc_spc_seq_page_cost);

	/* 如果使用NextSampleBlock，则假设随机访问，否则顺序访问 */
	fc_spc_page_cost = (fc_tsm->NextSampleBlock != NULL) ?
		fc_spc_random_page_cost : fc_spc_seq_page_cost;

	/*
	 * 磁盘成本（请记住，baserel->pages已经设置为
	 * 采样方法将访问的页面数量）
	 */
	fc_run_cost += fc_spc_page_cost * fc_baserel->pages;

	/*
	 * CPU成本（请记住，baserel->tuples已经设置为
	 * 采样方法将选择的元组数量）。注意，我们忽略了
	 * TABLESAMPLE参数表达式的执行成本；它们每次扫描只会
	 * 评估一次，在大多数情况下，它们可能是
	 * 简单常量。我们也不对采样方法内部可能进行的计算收取费用。
	 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_baserel->tuples;
	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->pathtarget->cost.startup;
	fc_run_cost += fc_path->pathtarget->cost.per_tuple * fc_path->rows;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_gather
 *	  确定并返回聚合路径的成本。
 *
 * 'rel'是要操作的关系
 * 'param_info'是该路径为参数化路径时的ParamPathInfo，否则为NULL
 * 'rows'可以用于指向行估计；如果不为NULL，它将覆盖
 * 'rel'和'param_info'。当路径并不完全
 * 对应于任何特定的RelOptInfo时，这很有用。
 */
void cost_gather(GatherPath *fc_path, PlannerInfo *fc_root,
			RelOptInfo *fc_rel, ParamPathInfo *fc_param_info,
			double *fc_rows)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;

	/* 使用正确的行估计标记路径 */
	if (fc_rows)
		fc_path->path.rows = *fc_rows;
	else if (fc_param_info)
		fc_path->path.rows = fc_param_info->ppi_rows;
	else
		fc_path->path.rows = fc_rel->rows;

	fc_startup_cost = fc_path->subpath->startup_cost;

	fc_run_cost = fc_path->subpath->total_cost - fc_path->subpath->startup_cost;

	/* 并行设置和通信成本。 */
	fc_startup_cost += parallel_setup_cost;
	fc_run_cost += parallel_tuple_cost * fc_path->path.rows;

	fc_path->path.startup_cost = fc_startup_cost;
	fc_path->path.total_cost = (fc_startup_cost + fc_run_cost);
}

/*
 * cost_gather_merge
 *	  确定并返回聚合合并路径的成本。
 *
 * GatherMerge合并几个预排序的输入流，使用一个堆，在
 * 每个给定时刻，堆中保持来自每个流的下一个元组。如果有N
 * 个流，我们大约需要N*log2(N)次元组比较来构建堆
 * 并在启动时，然后对于每个输出元组，大约需要log2(N)次比较，
 * 以用来自同一流的下一个元组替换顶部堆条目。
 */
void cost_gather_merge(GatherMergePath *fc_path, PlannerInfo *fc_root,
				  RelOptInfo *fc_rel, ParamPathInfo *fc_param_info,
				  Cost fc_input_startup_cost, Cost fc_input_total_cost,
				  double *fc_rows)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	Cost		fc_comparison_cost;
	double		fc_N;
	double		fc_logN;

	/* 使用正确的行估计标记路径 */
	if (fc_rows)
		fc_path->path.rows = *fc_rows;
	else if (fc_param_info)
		fc_path->path.rows = fc_param_info->ppi_rows;
	else
		fc_path->path.rows = fc_rel->rows;

	if (!enable_gathermerge)
		fc_startup_cost += disable_cost;

	/*
	 * 将工人的数量加一，以考虑领导者。尽管这可能
	 * 过于大方，因为领导者在典型情况下的工作量会少于其他工人，
	 * 但我们现在就这样做。
	 */
	Assert(fc_path->num_workers > 0);
	fc_N = (double) fc_path->num_workers + 1;
	fc_logN = LOG2(fc_N);

	/* 每次元组比较的假定成本 */
	fc_comparison_cost = 2.0 * cpu_operator_cost;

	/* 堆创建成本 */
	fc_startup_cost += fc_comparison_cost * fc_N * fc_logN;

	/* 每元组堆维护成本 */
	fc_run_cost += fc_path->path.rows * fc_comparison_cost * fc_logN;

	/* heap管理的小成本，例如cost_merge_append */
	fc_run_cost += cpu_operator_cost * fc_path->path.rows;

	/*
	 * 并行设置和通信成本。由于Gather Merge与Gather不同，
	 * 需要我们在所有工作进程的元组可用时进行阻塞，
	 * 因此与Gather相比，我们稍微增加了IPC成本。
	 * 出于缺乏更好的主意，额外收取5%的费用。
	 */
	fc_startup_cost += parallel_setup_cost;
	fc_run_cost += parallel_tuple_cost * fc_path->path.rows * 1.05;

	fc_path->path.startup_cost = fc_startup_cost + fc_input_startup_cost;
	fc_path->path.total_cost = (fc_startup_cost + fc_run_cost + fc_input_total_cost);
}

/*
 * cost_index
 *	  确定并返回使用索引扫描关系的成本。
 *
 * 'path'描述了待考虑的indexscan，并且除了
 *		由此例程设置的字段之外是完整的
 * 'loop_count'是需要纳入缓存行为估计的indexscan重复次数
 *
 * 除了行、startup_cost和total_cost，cost_index()还设置
 * 路径的indextotalcost和indexselectivity字段。这些值将在
 * IndexPath用于BitmapIndexScan时需要。
 *
 * 注意：path->indexquals必须仅包含可用作索引
 * 限制的子句。任何额外作为qpquals评估的条件可能会减少返回的
 * 元组数量，但不会减少我们必须从表中提取的元组数量，
 * 因此不会减少扫描成本。
 */
void cost_index(IndexPath *fc_path, PlannerInfo *fc_root, double fc_loop_count,
		   bool fc_partial_path)
{
	IndexOptInfo *fc_index = fc_path->indexinfo;
	RelOptInfo *fc_baserel = fc_index->rel;
	bool		fc_indexonly = (fc_path->path.pathtype == T_IndexOnlyScan);
	amcostestimate_function fc_amcostestimate;
	List	   *fc_qpquals;
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	Cost		fc_cpu_run_cost = 0;
	Cost		fc_indexStartupCost;
	Cost		fc_indexTotalCost;
	Selectivity fc_indexSelectivity;
	double		fc_indexCorrelation,
				fc_csquared;
	double		fc_spc_seq_page_cost,
				fc_spc_random_page_cost;
	Cost		fc_min_IO_cost,
				fc_max_IO_cost;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;
	double		fc_tuples_fetched;
	double		fc_pages_fetched;
	double		fc_rand_heap_pages;
	double		fc_index_pages;

	/* 应仅应用于基础关系 */
	Assert(IsA(fc_baserel, RelOptInfo) &&
		   IsA(fc_index, IndexOptInfo));
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_RELATION);

	/*
	 * 用正确的行估计标记路径，并识别哪些限制
	 * 需要作为qpquals强制执行。我们不需要检查任何由
	 * 索引的谓词隐含的限制，因此我们可以使用indrestrictinfo而不是
	 * baserestrictinfo作为关系的相关限制子句列表。
	 */
	if (fc_path->path.param_info)
	{
		fc_path->path.rows = fc_path->path.param_info->ppi_rows;
		/* qpquals来自关系的限制子句和ppi_clauses */
		fc_qpquals = list_concat(fc_extract_nonindex_conditions(fc_path->indexinfo->indrestrictinfo,
														  fc_path->indexclauses),
							  fc_extract_nonindex_conditions(fc_path->path.param_info->ppi_clauses,
														  fc_path->indexclauses));
	}
	else
	{
		fc_path->path.rows = fc_baserel->rows;
		/* qpquals仅来自关系的限制子句 */
		fc_qpquals = fc_extract_nonindex_conditions(fc_path->indexinfo->indrestrictinfo,
											  fc_path->indexclauses);
	}

	if (!enable_indexscan)
		fc_startup_cost += disable_cost;
	/* 我们不需要检查enable_indexonlyscan；indxpath.c会处理 */

	/*
	 * 调用索引访问方法特定代码以估计扫描索引的处理成本，
	 * 以及索引的选择性（即，我们将不得不检索的主表元组的比例）
	 * 和它与主表元组顺序的相关性。我们在这里需要一个类型转换，
	 * 因为pathnodes.h使用了一个弱函数类型以避免包含amapi.h。
	 */
	fc_amcostestimate = (amcostestimate_function) fc_index->amcostestimate;
	fc_amcostestimate(fc_root, fc_path, fc_loop_count,
				   &fc_indexStartupCost, &fc_indexTotalCost,
				   &fc_indexSelectivity, &fc_indexCorrelation,
				   &fc_index_pages);

	/*
	 * 保存 amcostestimate 的结果以便可能在位图扫描规划中使用。
	 * 我们不必保存indexStartupCost或indexCorrelation，
	 * 因为位图扫描不关心这两者。
	 */
	fc_path->indextotalcost = fc_indexTotalCost;
	fc_path->indexselectivity = fc_indexSelectivity;

	/* 所有触及索引本身的成本包含在此 */
	fc_startup_cost += fc_indexStartupCost;
	fc_run_cost += fc_indexTotalCost - fc_indexStartupCost;

	/* 估计提取的主表元组数量 */
	fc_tuples_fetched = clamp_row_est(fc_indexSelectivity * fc_baserel->tuples);

	/* 提取包含表的表空间的估计页成本 */
	get_tablespace_page_costs(fc_baserel->reltablespace,
							  &fc_spc_random_page_cost,
							  &fc_spc_seq_page_cost);

	/*----------
	 * 估计提取的主表页数量，并计算I/O成本。
	 *
	 * 当索引顺序与表顺序不相关时，
	 * 我们使用Mackert和Lohman提出的近似值（请参阅
	 * index_pages_fetched()获取详细信息）来计算提取的页数，
	 * 然后对每提取的页按spc_random_page_cost收费。
	 *
	 * 当索引顺序与表顺序完全相关时
	 * （例如刚进行CLUSTER之后），提取的页数应该
	 * 恰好为selectivity * table_size。此外，除了第一
	 * 页，其他的都将是顺序提取，而不是在
	 * 不相关情况下发生的随机提取。因此如果页数超过1，
	 * 我们应该收取
	 *		spc_random_page_cost + (pages_fetched - 1) * spc_seq_page_cost
	 * 对于部分相关的索引，我们应该在
	 * 这两个估计之间收费。目前我们根据相关性平方在这两个
	 * 估计之间线性插值（XXX这合适吗？）。
	 *
	 * 如果是索引仅扫描，则我们不需要提取任何可见图
	 * 显示所有元组都是可见的堆页。
	 * 因此，相应减少估计的堆提取数量。
	 * 我们使用整个堆的测量比例，该堆全部可见，
	 * 这可能与该查询将提取的堆子集不太相关；但不清楚
	 * 如何做得更好。
	 *----------
	 */
	if (fc_loop_count > 1)
	{
		/*
		 * 对于重复的indexscans，适当的估计是将
		 * Mackert和Lohman公式中提取的元组数量按扫描次数
		 * 缩放，以便估计所有扫描提取的页数；然后
		 * 按比例分配一次扫描的成本。在这种情况下，
		 * 我们假定所有提取都是随机访问。
		 */
		fc_pages_fetched = index_pages_fetched(fc_tuples_fetched * fc_loop_count,
											fc_baserel->pages,
											(double) fc_index->pages,
											fc_root);

		if (fc_indexonly)
			fc_pages_fetched = ceil(fc_pages_fetched * (1.0 - fc_baserel->allvisfrac));

		fc_rand_heap_pages = fc_pages_fetched;

		fc_max_IO_cost = (fc_pages_fetched * fc_spc_random_page_cost) / fc_loop_count;

		
/*
		 * 在完全相关的情况下，每次扫描触及的页面数量为
		 * selectivity * table_size，我们可以在页面级别使用Mackert
		 * 和Lohman公式来估计通过缓存跨扫描节省了多少工作量。我们仍然假设所有提取都是
		 * 随机的，这实际上是一个高估，难以在不重复计算缓存效果的情况下进行修正。
		 * （但在大多数情况下，如果这样的计划确实有趣，每次扫描也只会提取一页，
		 * 所以这应该没什么大不了的。）
		 */
		fc_pages_fetched = ceil(fc_indexSelectivity * (double) fc_baserel->pages);

		fc_pages_fetched = index_pages_fetched(fc_pages_fetched * fc_loop_count,
											fc_baserel->pages,
											(double) fc_index->pages,
											fc_root);

		if (fc_indexonly)
			fc_pages_fetched = ceil(fc_pages_fetched * (1.0 - fc_baserel->allvisfrac));

		fc_min_IO_cost = (fc_pages_fetched * fc_spc_random_page_cost) / fc_loop_count;
	}
	else
	{
		/*
		 * 正常情况：应用Mackert和Lohman公式，然后
		 * 在此结果与由相关性得出的结果之间进行插值。
		 */
		fc_pages_fetched = index_pages_fetched(fc_tuples_fetched,
											fc_baserel->pages,
											(double) fc_index->pages,
											fc_root);

		if (fc_indexonly)
			fc_pages_fetched = ceil(fc_pages_fetched * (1.0 - fc_baserel->allvisfrac));

		fc_rand_heap_pages = fc_pages_fetched;

		/* max_IO_cost是指完全不相关的情况（csquared=0） */
		fc_max_IO_cost = fc_pages_fetched * fc_spc_random_page_cost;

		/* min_IO_cost是指完全相关的情况（csquared=1） */
		fc_pages_fetched = ceil(fc_indexSelectivity * (double) fc_baserel->pages);

		if (fc_indexonly)
			fc_pages_fetched = ceil(fc_pages_fetched * (1.0 - fc_baserel->allvisfrac));

		if (fc_pages_fetched > 0)
		{
			fc_min_IO_cost = fc_spc_random_page_cost;
			if (fc_pages_fetched > 1)
				fc_min_IO_cost += (fc_pages_fetched - 1) * fc_spc_seq_page_cost;
		}
		else
			fc_min_IO_cost = 0;
	}

	if (fc_partial_path)
	{
		/*
		 * 对于仅索引扫描，根据提取的索引页面数量计算工作线程；
		 * 我们提取的堆页面数量可能非常小，实际上排除了并行性，这是我们不想要的。
		 */
		if (fc_indexonly)
			fc_rand_heap_pages = -1;

		/*
		 * 估计扫描索引所需的并行工作线程数量。使用
		 * 考虑到堆提取不会像并行扫描那样顺序访问的堆页面数量进行计算。
		 */
		fc_path->path.parallel_workers = compute_parallel_worker(fc_baserel,
															  fc_rand_heap_pages,
															  fc_index_pages,
															  max_parallel_workers_per_gather);

		/*
		 * 如果无法分配工作线程进行并行扫描，则跳出，
		 * 因为在这种情况下，将拒绝此路径。因此进行额外计算没有益处。
		 */
		if (fc_path->path.parallel_workers <= 0)
			return;

		fc_path->path.parallel_aware = true;
	}

	/*
	 * 现在根据估计的索引顺序相关性插值，以获取主表访问的总
	 * 磁盘I/O成本。
	 */
	fc_csquared = fc_indexCorrelation * fc_indexCorrelation;

	fc_run_cost += fc_max_IO_cost + fc_csquared * (fc_min_IO_cost - fc_max_IO_cost);

	/*
	 * 估计每个元组的CPU成本。
	 *
	 * 我们在这里想要的是cpu_tuple_cost加上必须作为qpquals
	 * 评估的任何qual子句的评估成本。
	 */
	cost_qual_eval(&fc_qpqual_cost, fc_qpquals, fc_root);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple;

	fc_cpu_run_cost += fc_cpu_per_tuple * fc_tuples_fetched;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->path.pathtarget->cost.startup;
	fc_cpu_run_cost += fc_path->path.pathtarget->cost.per_tuple * fc_path->path.rows;

	/* 如果使用并行性，则调整成本。 */
	if (fc_path->path.parallel_workers > 0)
	{
		double		fc_parallel_divisor = get_parallel_divisor(&fc_path->path);

		fc_path->path.rows = clamp_row_est(fc_path->path.rows / fc_parallel_divisor);

		/* CPU成本在所有工作者之间分摊。 */
		fc_cpu_run_cost /= fc_parallel_divisor;
	}

	fc_run_cost += fc_cpu_run_cost;

	fc_path->path.startup_cost = fc_startup_cost;
	fc_path->path.total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * extract_nonindex_conditions
 *
 * 给定一个在indexscan中要强制执行的qual列表，提取那些
 * 必须作为qpquals应用的条件（即，索引机制将无法处理
 * 它们）。在这里，我们仅检测qual子句是否与某些indexclause
 * 直接冗余。如果选择了索引路径，createplan.c将更努力地
 * 去除冗余的qual条件；具体而言，它将看看qual是否可以
 * 证明被indexquals所隐含。但在这个阶段尝试考虑这一点
 * 并不值得消耗周期，因为我们只是想估计qual评估成本。
 * 否则这必须与create_indexscan_plan()中的逻辑匹配。
 *
 * qual_clauses和结果是RestrictInfos的列表。
 * indexclauses是IndexClauses的列表。
 */
static List * fc_extract_nonindex_conditions(List *fc_qual_clauses, List *fc_indexclauses)
{
	List	   *fc_result = NIL;
	ListCell   *fc_lc;

	foreach(fc_lc, fc_qual_clauses)
	{
		RestrictInfo *fc_rinfo = lfirst_node(RestrictInfo, fc_lc);

		if (fc_rinfo->pseudoconstant)
			continue;			/* 我们可以在这里丢掉伪常量 */
		if (is_redundant_with_indexclauses(fc_rinfo, fc_indexclauses))
			continue;			/* 重复或派生自同一等价类 */
		/* ... 跳过predicate证明attempt createplan.c will try ... */
		fc_result = lappend(fc_result, fc_rinfo);
	}
	return fc_result;
}

/*
 * index_pages_fetched
 *	  估计在考虑到
 *	  缓存效应后实际提取的页面数量。
 *
 * 我们使用Mackert和Lohman提出的近似值，“使用有限LRU缓冲区的索引扫描：
 * 一种经过验证的I/O模型”，ACM数据库系统交易，第14卷，第3期，1989年9月，第401-424页。
 * Mackert和Lohman的近似值是提取的页面数量为
 *	PF =
 *		min(2TNs/(2T+Ns), T)			当T <= b
 *		2TNs/(2T+Ns)					当T > b且Ns <= 2Tb/(2T-b)
 *		b + (Ns - 2Tb/(2T-b))*(T-b)/T	当T > b且Ns > 2Tb/(2T-b)
 * 其中
 *		T = 表中的页面数量
 *		N = 表中的元组数量
 *		s = selectivity = 要扫描的表的比例
 *		b = 可用缓冲页面数量（我们在这里包括内核空间）
 *
 * 我们假设effective_cache_size是整个查询可用的总缓冲页面
 * 数量，并在查询中的所有表和当前考虑的索引之间按比例分配
 * 该空间。（这忽略了查询使用的其他索引所需的空间，
 * 但由于我们不知道将使用哪些索引，因此无法很好地估计；
 * 而且在任何情况下，计算所有表可能是高估，
 * 因为根据连接计划，并非所有表可能会被同时扫描。）
 *
 * 乘积Ns是提取的元组数量；我们传入该乘积
 * 而不是在此处计算。“pages”是考虑对象中的页面
 * 数量（无论是索引还是表）。“index_pages”是应
 * 添加到总表空间的数量，这个数量是由make_one_rel为我们计算的。
 *
 * 调用者应确保tuples_fetched大于零
 * 并四舍五入为整数（请参见clamp_row_est）。结果也将大于零且为整数。
 */
double index_pages_fetched(double fc_tuples_fetched, BlockNumber fc_pages,
					double fc_index_pages, PlannerInfo *fc_root)
{
	double		fc_pages_fetched;
	double		fc_total_pages;
	double		fc_T,
				fc_b;

	/* T是表中的页面数量，但不允许为零 */
	fc_T = (fc_pages > 1) ? (double) fc_pages : 1.0;

	/* 计算假定竞争缓存空间的页面数量 */
	fc_total_pages = fc_root->total_table_pages + fc_index_pages;
	fc_total_pages = Max(fc_total_pages, 1.0);
	Assert(fc_T <= fc_total_pages);

	/* b 是有效缓存大小的按比例份额 */
	fc_b = (double) effective_cache_size * fc_T / fc_total_pages;

	/* 强制其为正整数 */
	if (fc_b <= 1.0)
		fc_b = 1.0;
	else
		fc_b = ceil(fc_b);

	/* 这一部分是 Mackert 和 Lohman 公式 */
	if (fc_T <= fc_b)
	{
		fc_pages_fetched =
			(2.0 * fc_T * fc_tuples_fetched) / (2.0 * fc_T + fc_tuples_fetched);
		if (fc_pages_fetched >= fc_T)
			fc_pages_fetched = fc_T;
		else
			fc_pages_fetched = ceil(fc_pages_fetched);
	}
	else
	{
		double		fc_lim;

		fc_lim = (2.0 * fc_T * fc_b) / (2.0 * fc_T - fc_b);
		if (fc_tuples_fetched <= fc_lim)
		{
			fc_pages_fetched =
				(2.0 * fc_T * fc_tuples_fetched) / (2.0 * fc_T + fc_tuples_fetched);
		}
		else
		{
			fc_pages_fetched =
				fc_b + (fc_tuples_fetched - fc_lim) * (fc_T - fc_b) / fc_T;
		}
		fc_pages_fetched = ceil(fc_pages_fetched);
	}
	return fc_pages_fetched;
}

/*
 * get_indexpath_pages
 *		确定位图索引路径中使用的索引的总大小。
 *
 * 注意：如果在位图树中多次使用相同的索引，我们将
 * 多次计数，这可能是不正确的……但目前还不完全清楚，
 * 检测重复值也很困难，因此现在忽略它。
 */
static double get_indexpath_pages(Path *fc_bitmapqual)
{
	double		fc_result = 0;
	ListCell   *fc_l;

	if (IsA(fc_bitmapqual, BitmapAndPath))
	{
		BitmapAndPath *fc_apath = (BitmapAndPath *) fc_bitmapqual;

		foreach(fc_l, fc_apath->bitmapquals)
		{
			fc_result += get_indexpath_pages((Path *) lfirst(fc_l));
		}
	}
	else if (IsA(fc_bitmapqual, BitmapOrPath))
	{
		BitmapOrPath *fc_opath = (BitmapOrPath *) fc_bitmapqual;

		foreach(fc_l, fc_opath->bitmapquals)
		{
			fc_result += get_indexpath_pages((Path *) lfirst(fc_l));
		}
	}
	else if (IsA(fc_bitmapqual, IndexPath))
	{
		IndexPath  *fc_ipath = (IndexPath *) fc_bitmapqual;

		fc_result = (double) fc_ipath->indexinfo->pages;
	}
	else
		elog(ERROR, "unrecognized node type: %d", nodeTag(fc_bitmapqual));

	return fc_result;
}

/*
 * cost_bitmap_heap_scan
 *	  确定并返回使用位图
 *	  索引然后堆的扫描代价。
 *
 * 'baserel' 是要扫描的关系
 * 'param_info' 是 ParamPathInfo，如果这是一个参数化路径，则为 NULL
 * 'bitmapqual' 是索引路径、BitmapAndPaths 和 BitmapOrPaths 的树
 * 'loop_count' 是计入
 *		缓存行为估算的索引扫描重复次数
 *
 * 注意：bitmapqual 中的各个 IndexPaths 应该使用
 * 相同的 loop_count 进行计费。
 */
void cost_bitmap_heap_scan(Path *fc_path, PlannerInfo *fc_root, RelOptInfo *fc_baserel,
					  ParamPathInfo *fc_param_info,
					  Path *fc_bitmapqual, double fc_loop_count)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	Cost		fc_indexTotalCost;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;
	Cost		fc_cost_per_page;
	Cost		fc_cpu_run_cost;
	double		fc_tuples_fetched;
	double		fc_pages_fetched;
	double		fc_spc_seq_page_cost,
				fc_spc_random_page_cost;
	double		fc_T;

	/* 应仅应用于基础关系 */
	Assert(IsA(fc_baserel, RelOptInfo));
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_RELATION);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	if (!enable_bitmapscan)
		fc_startup_cost += disable_cost;

	fc_pages_fetched = compute_bitmap_pages(fc_root, fc_baserel, fc_bitmapqual,
										 fc_loop_count, &fc_indexTotalCost,
										 &fc_tuples_fetched);

	fc_startup_cost += fc_indexTotalCost;
	fc_T = (fc_baserel->pages > 1) ? (double) fc_baserel->pages : 1.0;

	/* 获取包含表的表空间的估计页面成本。 */
	get_tablespace_page_costs(fc_baserel->reltablespace,
							  &fc_spc_random_page_cost,
							  &fc_spc_seq_page_cost);

	/*
	 * 对于少量页面，我们应该按每个页面收取 spc_random_page_cost，
	 * 而如果几乎读取了表的所有页面，则更合适按每个页面收取
	 * spc_seq_page_cost。效果也是
	 * 非线性的。目前没有更好的想法，以这种方式插值来
	 * 确定每页的成本。
	 */
	if (fc_pages_fetched >= 2.0)
		fc_cost_per_page = fc_spc_random_page_cost -
			(fc_spc_random_page_cost - fc_spc_seq_page_cost)
			* sqrt(fc_pages_fetched / fc_T);
	else
		fc_cost_per_page = fc_spc_random_page_cost;

	fc_run_cost += fc_pages_fetched * fc_cost_per_page;

	/*
	 * 估算每个元组的 CPU 成本。
	 *
	 * 通常，indexquals 不需要在每个元组上重新检查……但
	 * 不是总是这样，尤其是在涉及足够多个元组以至于
	 * 位图变得有损失的情况下。此时，仅假设它们会
	 * 总是被重新检查。这意味着我们对所有的
	 * 扫描子句都收取全部费用。
	 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_cpu_run_cost = fc_cpu_per_tuple * fc_tuples_fetched;

	/* 如果使用并行性，则调整成本。 */
	if (fc_path->parallel_workers > 0)
	{
		double		fc_parallel_divisor = get_parallel_divisor(fc_path);

		/* CPU成本在所有工作者之间分摊。 */
		fc_cpu_run_cost /= fc_parallel_divisor;

		fc_path->rows = clamp_row_est(fc_path->rows / fc_parallel_divisor);
	}


	fc_run_cost += fc_cpu_run_cost;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->pathtarget->cost.startup;
	fc_run_cost += fc_path->pathtarget->cost.per_tuple * fc_path->rows;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_bitmap_tree_node
 *		从位图树节点中提取成本和选择性（索引/与/或）
 */
void cost_bitmap_tree_node(Path *fc_path, Cost *fc_cost, Selectivity *fc_selec)
{
	if (IsA(fc_path, IndexPath))
	{
		*fc_cost = ((IndexPath *) fc_path)->indextotalcost;
		*fc_selec = ((IndexPath *) fc_path)->indexselectivity;

		/*
		 * 对每个检索到的元组收取少量费用，以反映
		 * 操作位图的成本。这主要是为了确保位图
		 * 扫描的成本看起来与索引扫描以检索单个元组相同。
		 */
		*fc_cost += 0.1 * cpu_operator_cost * fc_path->rows;
	}
	else if (IsA(fc_path, BitmapAndPath))
	{
		*fc_cost = fc_path->total_cost;
		*fc_selec = ((BitmapAndPath *) fc_path)->bitmapselectivity;
	}
	else if (IsA(fc_path, BitmapOrPath))
	{
		*fc_cost = fc_path->total_cost;
		*fc_selec = ((BitmapOrPath *) fc_path)->bitmapselectivity;
	}
	else
	{
		elog(ERROR, "unrecognized node type: %d", nodeTag(fc_path));
		*fc_cost = *fc_selec = 0;		/* 保持编译器安静 */
	}
}

/*
 * cost_bitmap_and_node
 *		估算 BitmapAnd 节点的成本
 *
 * 注意，这仅考虑索引扫描和位图
 * 创建的成本，而不考虑最终的堆访问。从这个意义上说，
 * 对象并不真正是路径，但它具有足够的类似路径的属性
 *（尤其是成本）值得将其视为路径。然而，我们不打算设置路径行字段。
 */
void cost_bitmap_and_node(BitmapAndPath *fc_path, PlannerInfo *fc_root)
{
	Cost		fc_totalCost;
	Selectivity fc_selec;
	ListCell   *fc_l;

	/*
	 * 我们根据输入相互独立的假设来估计 AND 选择性。
	 * 这往往是错误的，但我们没有更好的信息来改进。
	 *
	 * BitmapAnd 本身的运行时成本估计为 100x
	 * cpu_operator_cost，针对每个需要的 tbm_intersect。可能太小，
	 * 绝对太简单？
	 */
	fc_totalCost = 0.0;
	fc_selec = 1.0;
	foreach(fc_l, fc_path->bitmapquals)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_l);
		Cost		fc_subCost;
		Selectivity fc_subselec;

		cost_bitmap_tree_node(fc_subpath, &fc_subCost, &fc_subselec);

		fc_selec *= fc_subselec;

		fc_totalCost += fc_subCost;
		if (fc_l != list_head(fc_path->bitmapquals))
			fc_totalCost += 100.0 * cpu_operator_cost;
	}
	fc_path->bitmapselectivity = fc_selec;
	fc_path->path.rows = 0;		/* 如上所述，未使用 */
	fc_path->path.startup_cost = fc_totalCost;
	fc_path->path.total_cost = fc_totalCost;
}

/*
 * cost_bitmap_or_node
 *		估算 BitmapOr 节点的成本
 *
 * 参见 cost_bitmap_and_node 的注释。
 */
void cost_bitmap_or_node(BitmapOrPath *fc_path, PlannerInfo *fc_root)
{
	Cost		fc_totalCost;
	Selectivity fc_selec;
	ListCell   *fc_l;

	/*
	 * 我们根据输入不重叠的假设来估计 OR 选择性，
	 * 因为在“x IN (list)”类型的情况下通常是这样的。
	 * 当然，最后我们将其限制为 1.0。
	 *
	 * BitmapOr 本身的运行时成本估计为 100x
	 * cpu_operator_cost，针对每个需要的 tbm_union。可能太小，
	 * 绝对太简单？我们知道，当输入是 BitmapIndexScans 时，
	 * tbm_unions 被优化掉了。
	 */
	fc_totalCost = 0.0;
	fc_selec = 0.0;
	foreach(fc_l, fc_path->bitmapquals)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_l);
		Cost		fc_subCost;
		Selectivity fc_subselec;

		cost_bitmap_tree_node(fc_subpath, &fc_subCost, &fc_subselec);

		fc_selec += fc_subselec;

		fc_totalCost += fc_subCost;
		if (fc_l != list_head(fc_path->bitmapquals) &&
			!IsA(fc_subpath, IndexPath))
			fc_totalCost += 100.0 * cpu_operator_cost;
	}
	fc_path->bitmapselectivity = Min(fc_selec, 1.0);
	fc_path->path.rows = 0;		/* 如上所述，未使用 */
	fc_path->path.startup_cost = fc_totalCost;
	fc_path->path.total_cost = fc_totalCost;
}

/*
 * cost_tidscan
 *	  确定并返回使用 TID 扫描关系的成本。
 *
 * 'baserel' 是要扫描的关系
 * 'tidquals' 是可以检查 TID 的条件列表
 * 'param_info' 是 ParamPathInfo，如果这是一个参数化路径，则为 NULL
 */
void cost_tidscan(Path *fc_path, PlannerInfo *fc_root,
			 RelOptInfo *fc_baserel, List *fc_tidquals, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	bool		fc_isCurrentOf = false;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;
	QualCost	fc_tid_qual_cost;
	int			fc_ntuples;
	ListCell   *fc_l;
	double		fc_spc_random_page_cost;

	/* 应仅应用于基础关系 */
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_RELATION);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	/* 计算我们预计将检索到多少元组 */
	fc_ntuples = 0;
	foreach(fc_l, fc_tidquals)
	{
		RestrictInfo *fc_rinfo = lfirst_node(RestrictInfo, fc_l);
		Expr	   *fc_qual = fc_rinfo->clause;

		if (IsA(fc_qual, ScalarArrayOpExpr))
		{
			/* 数组的每个元素产生 1 个元组 */
			ScalarArrayOpExpr *fc_saop = (ScalarArrayOpExpr *) fc_qual;
			Node	   *fc_arraynode = (Node *) lsecond(fc_saop->args);

			fc_ntuples += estimate_array_length(fc_arraynode);
		}
		else if (IsA(fc_qual, CurrentOfExpr))
		{
			/* CURRENT OF 产生 1 个元组 */
			fc_isCurrentOf = true;
			fc_ntuples++;
		}
		else
		{
			/* 这只是 CTID = something，计数 1 个元组 */
			fc_ntuples++;
		}
	}

	/*
	 * 对于 WHERE CURRENT OF，我们必须强制 TID 扫描，
	 * 因为只有 nodeTidscan.c 明白如何正确处理它。
	 * 因此，仅在 CURRENT OF 不存在时，我们才遵守 enable_tidscan。
	 * 另请注意，cost_qual_eval 将 CurrentOfExpr 计为启动成本的 
	 * disable_cost，我们在此减去；这是为了防止其他计划类型，如 seqscan，
	 * 获胜。
	 */
	if (fc_isCurrentOf)
	{
		Assert(fc_baserel->baserestrictcost.startup >= disable_cost);
		fc_startup_cost -= disable_cost;
	}
	else if (!enable_tidscan)
		fc_startup_cost += disable_cost;

	/*
	 * TID 资格表达式只会计算一次，而其他的 baserestrict
	 * 资格则会在每个检索到的元组中计算一次。
	 */
	cost_qual_eval(&fc_tid_qual_cost, fc_tidquals, fc_root);

	/* 获取包含表格的表空间的估计页面成本 */
	get_tablespace_page_costs(fc_baserel->reltablespace,
							  &fc_spc_random_page_cost,
							  NULL);

	/* 磁盘成本 --- 假设每个元组在不同的页面上 */
	fc_run_cost += fc_spc_random_page_cost * fc_ntuples;

	/* 添加扫描 CPU 成本 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	/* XXX 当前假设 TID 资格是 qpquals 的子集 */
	fc_startup_cost += fc_qpqual_cost.startup + fc_tid_qual_cost.per_tuple;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple -
		fc_tid_qual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_ntuples;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->pathtarget->cost.startup;
	fc_run_cost += fc_path->pathtarget->cost.per_tuple * fc_path->rows;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}


/*
 * cost_tidrangescan
 *	  确定并设置使用一系列 TIDs 扫描关系 'path' 的成本
 *
 * 'baserel' 是要扫描的关系
 * 'tidrangequals' 是可以检查 TID 的范围条件列表
 * 'param_info' 是 ParamPathInfo，如果这是一个参数化路径，则为此，否则为 NULL
 */
void cost_tidrangescan(Path *fc_path, PlannerInfo *fc_root,
				  RelOptInfo *fc_baserel, List *fc_tidrangequals,
				  ParamPathInfo *fc_param_info)
{
	Selectivity fc_selectivity;
	double		fc_pages;
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;
	QualCost	fc_tid_qual_cost;
	double		fc_ntuples;
	double		fc_nseqpages;
	double		fc_spc_random_page_cost;
	double		fc_spc_seq_page_cost;

	/* 应仅应用于基础关系 */
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_RELATION);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	/* 计算我们预期扫描的元组和页面数 */
	fc_selectivity = clauselist_selectivity(fc_root, fc_tidrangequals, fc_baserel->relid,
										 JOIN_INNER, NULL);
	fc_pages = ceil(fc_selectivity * fc_baserel->pages);

	if (fc_pages <= 0.0)
		fc_pages = 1.0;

	/*
	 * 范围中的第一页需要随机寻址，但后续每一页只是正常的顺序页面读取。注意：TID 范围扫描的成本应该高于等效的顺序扫描，
	 * 因为顺序扫描在某些性能上具有优势，例如扫描同步和可并行性，我们希望选择其中一个，除非 TID 范围扫描确实更好。
	 */
	fc_ntuples = fc_selectivity * fc_baserel->tuples;
	fc_nseqpages = fc_pages - 1.0;

	if (!enable_tidscan)
		fc_startup_cost += disable_cost;

	/*
	 * TID 资格表达式只会计算一次，而其他的 baserestrict
	 * 资格则会在每个检索到的元组中计算一次。
	 */
	cost_qual_eval(&fc_tid_qual_cost, fc_tidrangequals, fc_root);

	/* 获取包含表格的表空间的估计页面成本 */
	get_tablespace_page_costs(fc_baserel->reltablespace,
							  &fc_spc_random_page_cost,
							  &fc_spc_seq_page_cost);

	/* 磁盘成本；1 个随机页面和其余的作为顺序页面 */
	fc_run_cost += fc_spc_random_page_cost + fc_spc_seq_page_cost * fc_nseqpages;

	/* 添加扫描 CPU 成本 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	/*
	 * XXX 当前我们假设 TID 资格是在此时 qpquals 的一个子集；当我们创建计划时，它们将被移除（如果可能），因此我们从总 qqual 成本中减去它们的成本。 （如果 TID 资格无法被移除，这就是一个错误，我们将低估 CPU 成本。）
	 */
	fc_startup_cost += fc_qpqual_cost.startup + fc_tid_qual_cost.per_tuple;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple -
		fc_tid_qual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_ntuples;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->pathtarget->cost.startup;
	fc_run_cost += fc_path->pathtarget->cost.per_tuple * fc_path->rows;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_subqueryscan
 *	  确定并返回扫描子查询 RTE 的成本。
 *
 * 'baserel' 是要扫描的关系
 * 'param_info' 是 ParamPathInfo，如果这是一个参数化路径，否则为 NULL
 */
void cost_subqueryscan(SubqueryScanPath *fc_path, PlannerInfo *fc_root,
				  RelOptInfo *fc_baserel, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost;
	Cost		fc_run_cost;
	List	   *fc_qpquals;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;

	/* 只应适用于作为子查询的基础关系 */
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_SUBQUERY);

	/*
	 * 我们计算行数估计值为子计划的估计值乘以相关限制子句的选择性。在简单情况下，这将与 baserel->rows 相同；但是在处理并行化路径时，我们必须这样做才能获得正确答案。
	 */
	if (fc_param_info)
		fc_qpquals = list_concat_copy(fc_param_info->ppi_clauses,
								   fc_baserel->baserestrictinfo);
	else
		fc_qpquals = fc_baserel->baserestrictinfo;

	fc_path->path.rows = clamp_row_est(fc_path->subpath->rows *
									clauselist_selectivity(fc_root,
														   fc_qpquals,
														   0,
														   JOIN_INNER,
														   NULL));

	/*
	 * 路径的成本是评估子计划的成本，加上评估将附加到 SubqueryScan 节点的任何限制子句和 tlist 的成本，加上 cpu_tuple_cost 以考虑选择和投影的开销。
	 */
	fc_path->path.startup_cost = fc_path->subpath->startup_cost;
	fc_path->path.total_cost = fc_path->subpath->total_cost;

	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost = fc_qpqual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_run_cost = fc_cpu_per_tuple * fc_path->subpath->rows;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->path.pathtarget->cost.startup;
	fc_run_cost += fc_path->path.pathtarget->cost.per_tuple * fc_path->path.rows;

	fc_path->path.startup_cost += fc_startup_cost;
	fc_path->path.total_cost += fc_startup_cost + fc_run_cost;
}

/*
 * cost_functionscan
 *	  确定并返回扫描函数 RTE 的成本。
 *
 * 'baserel' 是要扫描的关系
 * 'param_info' 是 ParamPathInfo，如果这是一个参数化路径，否则为 NULL
 */
void cost_functionscan(Path *fc_path, PlannerInfo *fc_root,
				  RelOptInfo *fc_baserel, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;
	RangeTblEntry *fc_rte;
	QualCost	fc_exprcost;

	/* 只应适用于作为函数的基础关系 */
	Assert(fc_baserel->relid > 0);
	fc_rte = planner_rt_fetch(fc_baserel->relid, fc_root);
	Assert(fc_rte->rtekind == RTE_FUNCTION);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	/*
	 * 估算执行函数表达式的成本。
	 *
	 * 当前，nodeFunctionscan.c 总是执行函数直到完成，然后再返回任何行，并将结果缓存在 tuplestore 中。因此，函数评估成本完全是启动成本，而每行成本很小。
	 *
	 * XXX 原则上，如果行数较多，我们应该计算 tuplestore 溢出成本。然而，考虑到我们对函数的行数估计往往有多不准确，在目前阶段进行这种细化没有太大意义。
	 */
	cost_qual_eval_node(&fc_exprcost, (Node *) fc_rte->functions, fc_root);

	fc_startup_cost += fc_exprcost.startup + fc_exprcost.per_tuple;

	/* 添加扫描 CPU 成本 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_baserel->tuples;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->pathtarget->cost.startup;
	fc_run_cost += fc_path->pathtarget->cost.per_tuple * fc_path->rows;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_tablefuncscan
 *	  确定并返回扫描表函数的成本。
 *
 * 'baserel' 是要扫描的关系
 * 'param_info' 是 ParamPathInfo，如果这是一个参数化路径，否则为 NULL
 */
void cost_tablefuncscan(Path *fc_path, PlannerInfo *fc_root,
				   RelOptInfo *fc_baserel, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;
	RangeTblEntry *fc_rte;
	QualCost	fc_exprcost;

	/* 只应适用于作为函数的基础关系 */
	Assert(fc_baserel->relid > 0);
	fc_rte = planner_rt_fetch(fc_baserel->relid, fc_root);
	Assert(fc_rte->rtekind == RTE_TABLEFUNC);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	/*
	 * 估算执行表函数表达式的成本。
	 *
	 * XXX 原则上，如果行数较多，我们应该计算 tuplestore 溢出成本。然而，考虑到我们对表函数的行数估计往往有多不准确，在目前阶段进行这种细化没有太大意义。
	 */
	cost_qual_eval_node(&fc_exprcost, (Node *) fc_rte->tablefunc, fc_root);

	fc_startup_cost += fc_exprcost.startup + fc_exprcost.per_tuple;

	/* 添加扫描 CPU 成本 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_baserel->tuples;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->pathtarget->cost.startup;
	fc_run_cost += fc_path->pathtarget->cost.per_tuple * fc_path->rows;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_valuesscan
 *	  确定并返回扫描 VALUES RTE 的成本。
 *
 * 'baserel' 是要扫描的关系
 * 'param_info' 是 ParamPathInfo，如果这是一个参数化路径，否则为 NULL
 */
void cost_valuesscan(Path *fc_path, PlannerInfo *fc_root,
				RelOptInfo *fc_baserel, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;

	/* 只应适用于作为值列表的基础关系 */
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_VALUES);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	/*
	 * 目前，估算列表评估成本为每个列表一个操作符评估（可能相当虚假，但值得更聪明吗？）
	 */
	fc_cpu_per_tuple = cpu_operator_cost;

	/* 添加扫描 CPU 成本 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple += cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_baserel->tuples;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->pathtarget->cost.startup;
	fc_run_cost += fc_path->pathtarget->cost.per_tuple * fc_path->rows;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_ctescan
 *	  确定并返回扫描 CTE RTE 的成本。
 *
 * 注意：这适用于自引用和常规 CTE；可能的成本差异低于我们能够准确估计的阈值。注意，评估引用的 CTE 查询的成本在最终计划中作为 initplan 成本添加，且不应在此计入。
 */
void cost_ctescan(Path *fc_path, PlannerInfo *fc_root,
			 RelOptInfo *fc_baserel, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;

	/* 只应适用于作为 CTE 的基础关系 */
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_CTE);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	/* 对于 tuplestore 操作收取每行一个 CPU 元组成本 */
	fc_cpu_per_tuple = cpu_tuple_cost;

	/* 添加扫描 CPU 成本 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple += cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_baserel->tuples;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->pathtarget->cost.startup;
	fc_run_cost += fc_path->pathtarget->cost.per_tuple * fc_path->rows;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_namedtuplestorescan
 *	  确定并返回扫描命名的 tuplestore 的成本。
 */
void cost_namedtuplestorescan(Path *fc_path, PlannerInfo *fc_root,
						 RelOptInfo *fc_baserel, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;

	/* 只应适用于作为 Tuplestores 的基础关系 */
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_NAMEDTUPLESTORE);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	/* 对于 tuplestore 操作收取每行一个 CPU 元组成本 */
	fc_cpu_per_tuple = cpu_tuple_cost;

	/* 添加扫描 CPU 成本 */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple += cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_baserel->tuples;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_resultscan
 *	  确定并返回扫描 RTE_RESULT 关系的成本。
 */
void cost_resultscan(Path *fc_path, PlannerInfo *fc_root,
				RelOptInfo *fc_baserel, ParamPathInfo *fc_param_info)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	QualCost	fc_qpqual_cost;
	Cost		fc_cpu_per_tuple;

	/* 只应适用于 RTE_RESULT 基础关系 */
	Assert(fc_baserel->relid > 0);
	Assert(fc_baserel->rtekind == RTE_RESULT);

	/* 使用正确的行估计标记路径 */
	if (fc_param_info)
		fc_path->rows = fc_param_info->ppi_rows;
	else
		fc_path->rows = fc_baserel->rows;

	/* 我们收取 qual 成本加 cpu_tuple_cost */
	get_restriction_qual_cost(fc_root, fc_baserel, fc_param_info, &fc_qpqual_cost);

	fc_startup_cost += fc_qpqual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qpqual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_baserel->tuples;

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_recursive_union
 *	  确定并返回执行递归并集的成本，以及预计的输出大小。
 *
 * 我们得到非递归和递归项的路径。
 */
void cost_recursive_union(Path *fc_runion, Path *fc_nrterm, Path *fc_rterm)
{
	Cost		fc_startup_cost;
	Cost		fc_total_cost;
	double		fc_total_rows;

	/* 我们可能对非递归项有合理的估计 */
	fc_startup_cost = fc_nrterm->startup_cost;
	fc_total_cost = fc_nrterm->total_cost;
	fc_total_rows = fc_nrterm->rows;

	/*
	 * 我们任意假设大约需要 10 次递归迭代，并且我们设法对每次迭代的成本和输出大小有较好的把握。这些假设非常脆弱，但很难看到如何做得更好。
	 */
	fc_total_cost += 10 * fc_rterm->total_cost;
	fc_total_rows += 10 * fc_rterm->rows;

	/*
	 * 每行也要收取 cpu_tuple_cost，以便考虑处理 tuplestore 的成本。（我们不担心可能的溢出到磁盘成本。）
	 */
	fc_total_cost += cpu_tuple_cost * fc_total_rows;

	fc_runion->startup_cost = fc_startup_cost;
	fc_runion->total_cost = fc_total_cost;
	fc_runion->rows = fc_total_rows;
	fc_runion->pathtarget->width = Max(fc_nrterm->pathtarget->width,
									fc_rterm->pathtarget->width);
}


/*
 * cost_tuplesort
 *	  确定并返回使用 tuplesort 排序一个关系的成本，不包括读取输入数据的成本。
 *
 * 如果要排序的数据总量小于 sort_mem，我们将进行内存排序，这不需要 I/O，并且对 t 个元组大约需要 t*log2(t) 次元组比较。
 *
 * 如果总量超过 sort_mem，我们将切换到磁带风格的合并算法。总的来说仍然大约有 t*log2(t) 次元组比较，但我们还需要在每次合并过程中读取和写入每个元组一次。我们预计会有大约 ceil(logM(r)) 次合并过程，其中 r 是形成的初始运行的数量，M 是由 tuplesort.c 使用的合并顺序。由于平均初始运行应该大约是 sort_mem，我们有
 *		磁盘流量 = 2 * relsize * ceil(logM(p / sort_mem))
 *		cpu = comparison_cost * t * log2(t)
 *
 * 如果排序是有界的（即，仅需要前 k 个结果元组），并且 k 个元组可以装入 sort_mem，我们使用一种堆方法，仅在堆中保留 k 个元组；这将大约需要 t*log2(k) 次元组比较。
 *
 * 假定磁盘流量的 3/4 是顺序的，1/4 是随机访问的（XXX 我们不能精细化这个猜测吗？）
 *
 * 默认情况下，我们对每次元组比较收取两次操作符评估的费用，在大多数情况下这应该在合适的范围内。调用者可以通过指定非零的 comparison_cost 来调整这一点；通常用于准备输入比较操作数所需的任何额外工作。
 *
 * 'tuples' 是关系中的元组数量
 * 'width' 是平均元组宽度（以字节为单位）
 * 'comparison_cost' 是每次比较的额外成本（如果有）
 * 'sort_mem' 是允许用于排序的工作内存的千字节数
 * 'limit_tuples' 是输出元组数量的限制；如果没有限制则为 -1
 */
static void fc_cost_tuplesort(Cost *fc_startup_cost, Cost *fc_run_cost,
			   double fc_tuples, int fc_width,
			   Cost fc_comparison_cost, int sort_mem,
			   double fc_limit_tuples)
{
	double		fc_input_bytes = fc_relation_byte_size(fc_tuples, fc_width);
	double		fc_output_bytes;
	double		fc_output_tuples;
	long		sort_mem_bytes = sort_mem * 1024L;

	/*
	 * 我们要确保排序的成本永远不会被估计为零，即使传入的元组数量为零。此外，不能计算 log(0)...
	 */
	if (fc_tuples < 2.0)
		fc_tuples = 2.0;

	/* 包括默认的每次比较成本 */
	fc_comparison_cost += 2.0 * cpu_operator_cost;

	/* 我们有一个有用的 LIMIT 吗？ */
	if (fc_limit_tuples > 0 && fc_limit_tuples < fc_tuples)
	{
		fc_output_tuples = fc_limit_tuples;
		fc_output_bytes = fc_relation_byte_size(fc_output_tuples, fc_width);
	}
	else
	{
		fc_output_tuples = fc_tuples;
		fc_output_bytes = fc_input_bytes;
	}

	if (fc_output_bytes > sort_mem_bytes)
	{
		/*
		 * 我们将不得不对所有元组使用基于磁盘的排序
		 */
		double		fc_npages = ceil(fc_input_bytes / BLCKSZ);
		double		fc_nruns = fc_input_bytes / sort_mem_bytes;
		double		fc_mergeorder = tuplesort_merge_order(sort_mem_bytes);
		double		fc_log_runs;
		double		fc_npageaccesses;

		/*
		 * CPU 成本
		 *
		 * 假设大约 N log2 N 次比较
		 */
		*fc_startup_cost = fc_comparison_cost * fc_tuples * LOG2(fc_tuples);

		/* 磁盘成本 */

		/* 将 logM(r) 计算为 log(r) / log(M) */
		if (fc_nruns > fc_mergeorder)
			fc_log_runs = ceil(log(fc_nruns) / log(fc_mergeorder));
		else
			fc_log_runs = 1.0;
		fc_npageaccesses = 2.0 * fc_npages * fc_log_runs;
		/* 假设 3/4 的访问是顺序的，1/4 不是 */
		*fc_startup_cost += fc_npageaccesses *
			(seq_page_cost * 0.75 + random_page_cost * 0.25);
	}
	else if (fc_tuples > 2 * fc_output_tuples || fc_input_bytes > sort_mem_bytes)
	{
		/*
		 * 我们将使用一个有界的堆排序，仅在内存中保留 K 个元组，
		 * 总的元组比较次数为 N log2 K；但是常数因子略高于快速排序。
		 * 调整它，以便成本曲线在交点处是连续的。
		 */
		*fc_startup_cost = fc_comparison_cost * fc_tuples * LOG2(2.0 * fc_output_tuples);
	}
	else
	{
		/* 我们将对所有输入元组使用简单的快速排序 */
		*fc_startup_cost = fc_comparison_cost * fc_tuples * LOG2(fc_tuples);
	}

	/*
	 * 还要对每个提取的元组收取少量费用（任意设置为运算符成本）。
	 * 我们不收取 cpu_tuple_cost，因为 Sort 节点不执行资格检查或投影，
	 * 因此它的开销低于大多数计划节点。请注意，在这里使用元组而不是
	 * output_tuples 是正确的——上限将按比例分配运行成本，否则
	 * 我们将重复计算 LIMIT。
	 */
	*fc_run_cost = cpu_operator_cost * fc_tuples;
}

/*
 * cost_incremental_sort
 * 	确定并返回逐步排序关系的成本，当输入路径按路径键的前缀排序时。
 *
 * 'presorted_keys' 是输入路径按其排序的前导路径键的数量。
 *
 * 我们通过领先的路径键估算关系被划分成的组数，然后使用
 * cost_tuplesort() 计算单个组的排序成本。
 */
void cost_incremental_sort(Path *fc_path,
					  PlannerInfo *fc_root, List *fc_pathkeys, int fc_presorted_keys,
					  Cost fc_input_startup_cost, Cost fc_input_total_cost,
					  double fc_input_tuples, int fc_width, Cost fc_comparison_cost, int sort_mem,
					  double fc_limit_tuples)
{
	Cost		fc_startup_cost = 0,
				fc_run_cost = 0,
				fc_input_run_cost = fc_input_total_cost - fc_input_startup_cost;
	double		fc_group_tuples,
				fc_input_groups;
	Cost		fc_group_startup_cost,
				fc_group_run_cost,
				fc_group_input_run_cost;
	List	   *fc_presortedExprs = NIL;
	ListCell   *fc_l;
	int			fc_i = 0;
	bool		fc_unknown_varno = false;

	Assert(fc_presorted_keys != 0);

	/*
	 * 我们要确保排序的成本永远不会被估计为零，即使传入的元组数量为零。此外，不能计算 log(0)...
	 */
	if (fc_input_tuples < 2.0)
		fc_input_tuples = 2.0;

	/* 默认估算的组数，限制为每行一个组。 */
	fc_input_groups = Min(fc_input_tuples, DEFAULT_NUM_DISTINCT);

	/*
	 * 提取预排序键作为表达式列表。
	 *
	 * 我们需要小心包含 "varno 0" 的 Vars，这可能是由 generate_append_tlist 引入的，
	 * 这将使 estimate_num_groups 混淆（实际上对于这样的表达式它会失败）。请参见
	 * recurse_set_operations，它必须处理相同的问题。
	 *
	 * 与 recurse_set_operations 不同，我们在这里无法访问原始目标列表，
	 * 即使我们可以，这对于组合多个表的集合操作的用途不是很明确。
	 * 因此，我们简单地检测是否存在任何包含 "varno 0" 的表达式，
	 * 在这种情况下使用默认的 DEFAULT_NUM_DISTINCT。
	 *
	 * 我们也可能使用 1.0（一个组）或 input_tuples（每行是一个单独的组），
	 * 几乎是增量排序的最坏和最佳情况。但这些都是极端情况，使用
	 * 中间的东西似乎是合理的。此外，generate_append_tlist 被用于集合操作，
	 * 这些操作可能最终会产生大部分唯一的输出——从这个角度看，DEFAULT_NUM_DISTINCT
	 * 是防御性的，同时保持较低的启动成本。
	 */
	foreach(fc_l, fc_pathkeys)
	{
		PathKey    *fc_key = (PathKey *) lfirst(fc_l);
		EquivalenceMember *fc_member = (EquivalenceMember *)
		linitial(fc_key->pk_eclass->ec_members);

		/*
		 * 检查表达式是否包含 "varno 0" 的 Var，以便我们在这情况下
		 * 不调用 estimate_num_groups。
		 */
		if (bms_is_member(0, pull_varnos(fc_root, (Node *) fc_member->em_expr)))
		{
			fc_unknown_varno = true;
			break;
		}

		/* 不包含任何 "varno 0" 的 Vars 的表达式 */
		fc_presortedExprs = lappend(fc_presortedExprs, fc_member->em_expr);

		fc_i++;
		if (fc_i >= fc_presorted_keys)
			break;
	}

	/* 估算具有相等的预排序键的组数。 */
	if (!fc_unknown_varno)
		fc_input_groups = estimate_num_groups(fc_root, fc_presortedExprs, fc_input_tuples,
										   NULL, NULL);

	fc_group_tuples = fc_input_tuples / fc_input_groups;
	fc_group_input_run_cost = fc_input_run_cost / fc_input_groups;

	/*
	 * 估算具有相等预排序键的单个组的平均排序成本。
	 * 增量排序对元组到组的分布敏感，我们在这里依赖非常粗略的假设。
	 * 因此，我们对增量排序性能持悲观态度，将其平均组大小增加一半。
	 */
	fc_cost_tuplesort(&fc_group_startup_cost, &fc_group_run_cost,
				   1.5 * fc_group_tuples, fc_width, fc_comparison_cost, sort_mem,
				   fc_limit_tuples);

	/*
	 * 增量排序的启动成本是其第一个组的启动成本加上其输入的成本。
	 */
	fc_startup_cost += fc_group_startup_cost
		+ fc_input_startup_cost + fc_group_input_run_cost;

	/*
	 * 在我们开始从第一个组生成元组后，生成所有元组的成本由完成处理
	 * 该组的成本，加上处理剩余组的总成本，加上剩余输入的成本给出。
	 */
	fc_run_cost += fc_group_run_cost
		+ (fc_group_run_cost + fc_group_startup_cost) * (fc_input_groups - 1)
		+ fc_group_input_run_cost * (fc_input_groups - 1);

	/*
	 * 增量排序本身增加了一些开销。首先，它必须检测排序组。
	 * 这大致等于每个元组额外的一个复制和比较。其次，它必须
	 * 对每个组重置元组排序上下文。
	 */
	fc_run_cost += (cpu_tuple_cost + fc_comparison_cost) * fc_input_tuples;
	fc_run_cost += 2.0 * cpu_tuple_cost * fc_input_groups;

	fc_path->rows = fc_input_tuples;
	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_sort
 *	  确定并返回排序关系的成本，包括读取输入数据的成本。
 *
 * 注意：一些调用者目前传递 NIL 作为路径键，因为他们
 * 不能方便地提供排序键。由于此例程当前无论如何不
 * 对路径键做任何处理，这并不重要……
 * 但如果它真的这样做，它应该优雅地对缺少键数据作出反应。
 * （实际上，我们最感兴趣的事情是排序键的数量，
 * 所有调用者 *都可以* 提供。）
 */
void cost_sort(Path *fc_path, PlannerInfo *fc_root,
		  List *fc_pathkeys, Cost fc_input_cost, double fc_tuples, int fc_width,
		  Cost fc_comparison_cost, int sort_mem,
		  double fc_limit_tuples)

{
	Cost		fc_startup_cost;
	Cost		fc_run_cost;

	fc_cost_tuplesort(&fc_startup_cost, &fc_run_cost,
				   fc_tuples, fc_width,
				   fc_comparison_cost, sort_mem,
				   fc_limit_tuples);

	if (!enable_sort)
		fc_startup_cost += disable_cost;

	fc_startup_cost += fc_input_cost;

	fc_path->rows = fc_tuples;
	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * append_nonpartial_cost
 *	  估算平行附加中的非部分路径的成本。
 *	  假设非部分路径是来自子路径列表的前 "numpaths" 个路径，
 *	  且按成本递减的顺序排列。
 */
static Cost fc_append_nonpartial_cost(List *fc_subpaths, int fc_numpaths, int fc_parallel_workers)
{
	Cost	   *fc_costarr;
	int			fc_arrlen;
	ListCell   *fc_l;
	ListCell   *fc_cell;
	int			fc_i;
	int			fc_path_index;
	int			fc_min_index;
	int			fc_max_index;

	if (fc_numpaths == 0)
		return 0;

	/*
	 * 数组长度为工作线程数或相关路径数，以较少者为准。
	 */
	fc_arrlen = Min(fc_parallel_workers, fc_numpaths);
	fc_costarr = (Cost *) palloc(sizeof(Cost) * fc_arrlen);

	/* 前几条路径将分别由不同的工作线程占用。 */
	fc_path_index = 0;
	foreach(fc_cell, fc_subpaths)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_cell);

		if (fc_path_index == fc_arrlen)
			break;
		fc_costarr[fc_path_index++] = fc_subpath->total_cost;
	}

	
/*
	 * 由于子路径按成本递减排序，最后一个子路径将具有
	 * 最小成本。
	 */
	fc_min_index = fc_arrlen - 1;

	/*
	 * 对于剩余的每一个子路径，将其成本添加到具有最低成本的数组元素中。
	 */
	for_each_cell(fc_l, fc_subpaths, fc_cell)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_l);
		int			fc_i;

		/* 仅考虑非部分路径 */
		if (fc_path_index++ == fc_numpaths)
			break;

		fc_costarr[fc_min_index] += fc_subpath->total_cost;

		/* 更新新的最小成本数组索引 */
		for (fc_min_index = fc_i = 0; fc_i < fc_arrlen; fc_i++)
		{
			if (fc_costarr[fc_i] < fc_costarr[fc_min_index])
				fc_min_index = fc_i;
		}
	}

	/* 返回数组中的最高成本 */
	for (fc_max_index = fc_i = 0; fc_i < fc_arrlen; fc_i++)
	{
		if (fc_costarr[fc_i] > fc_costarr[fc_max_index])
			fc_max_index = fc_i;
	}

	return fc_costarr[fc_max_index];
}

/*
 * cost_append
 *	  确定并返回一个 Append 节点的成本。
 */
void cost_append(AppendPath *fc_apath)
{
	ListCell   *fc_l;

	fc_apath->path.startup_cost = 0;
	fc_apath->path.total_cost = 0;
	fc_apath->path.rows = 0;

	if (fc_apath->subpaths == NIL)
		return;

	if (!fc_apath->path.parallel_aware)
	{
		List	   *fc_pathkeys = fc_apath->path.pathkeys;

		if (fc_pathkeys == NIL)
		{
			Path	   *fc_subpath = (Path *) linitial(fc_apath->subpaths);

			/*
			 * 对于一个无序的、非并行感知的 Append，我们将启动
			 * 成本视为第一个子路径的启动成本。
			 */
			fc_apath->path.startup_cost = fc_subpath->startup_cost;

			/* 按子计划的行和成本计算行和成本总和。 */
			foreach(fc_l, fc_apath->subpaths)
			{
				Path	   *fc_subpath = (Path *) lfirst(fc_l);

				fc_apath->path.rows += fc_subpath->rows;
				fc_apath->path.total_cost += fc_subpath->total_cost;
			}
		}
		else
		{
			/*
			 * 对于一个有序的、非并行感知的 Append，我们将启动
			 * 成本视为子路径启动成本的总和。这确保
			 * 当查询的 LIMIT 要求多个子路径运行以满足时
			 * 我们不会低估启动成本。这可能过于保守——
			 * 另一个合理的解决方案是将 Append 的启动成本视为
			 * 子路径启动成本的最大值。但我们不想冒险
			 * 认为一个 ORDER BY LIMIT 查询可以以低成本满足，
			 * 当第一个子路径具有较小的启动成本但后面的
			 * 却没有。(如果我们能够处理非线性成本
			 * 插值用于部分检索，我们就不需要对此如此保守。)
			 *
			 * 这个情况也与上述不同，因为我们必须
			 * 考虑可能将排序注入到本身不
			 * 是有序的子路径中。
			 */
			foreach(fc_l, fc_apath->subpaths)
			{
				Path	   *fc_subpath = (Path *) lfirst(fc_l);
				Path		sort_path;	/* cost_sort 结果的占位符 */

				if (!pathkeys_contained_in(fc_pathkeys, fc_subpath->pathkeys))
				{
					/*
					 * 我们需要插入一个排序节点，因此包括
					 * 相关的成本。如果父节点有LIMIT，我们可以使用它，
					 * 因为我们肯定不会从任何子节点中提取超过那么多的元组。
					 */
					cost_sort(&sort_path,
							  NULL, /* 目前不需要根节点 */
							  fc_pathkeys,
							  fc_subpath->total_cost,
							  fc_subpath->rows,
							  fc_subpath->pathtarget->width,
							  0.0,
							  work_mem,
							  fc_apath->limit_tuples);
					fc_subpath = &sort_path;
				}

				fc_apath->path.rows += fc_subpath->rows;
				fc_apath->path.startup_cost += fc_subpath->startup_cost;
				fc_apath->path.total_cost += fc_subpath->total_cost;
			}
		}
	}
	else						/* 并行感知 */
	{
		int			fc_i = 0;
		double		fc_parallel_divisor = get_parallel_divisor(&fc_apath->path);

		/* 并行感知追加从不产生有序输出。 */
		Assert(fc_apath->path.pathkeys == NIL);

		/* 计算启动成本。 */
		foreach(fc_l, fc_apath->subpaths)
		{
			Path	   *fc_subpath = (Path *) lfirst(fc_l);

			/*
			 * 当拥有最低启动成本的子节点完成设置时，追加开始返回元组。
			 * 我们仅考虑立即分配工作器的前几个子计划。
			 */
			if (fc_i == 0)
				fc_apath->path.startup_cost = fc_subpath->startup_cost;
			else if (fc_i < fc_apath->path.parallel_workers)
				fc_apath->path.startup_cost = Min(fc_apath->path.startup_cost,
											   fc_subpath->startup_cost);

			/*
			 * 将并行除数应用于子路径。根据给定子路径的
			 * 并行除数与我们采用的除数的比例，调整每个部分子路径的行数。
			 * 还应将部分路径的成本添加到总成本中，
			 * 但现在忽略非部分路径。
			 */
			if (fc_i < fc_apath->first_partial_path)
				fc_apath->path.rows += fc_subpath->rows / fc_parallel_divisor;
			else
			{
				double		fc_subpath_parallel_divisor;

				fc_subpath_parallel_divisor = get_parallel_divisor(fc_subpath);
				fc_apath->path.rows += fc_subpath->rows * (fc_subpath_parallel_divisor /
													 fc_parallel_divisor);
				fc_apath->path.total_cost += fc_subpath->total_cost;
			}

			fc_apath->path.rows = clamp_row_est(fc_apath->path.rows);

			fc_i++;
		}

		/* 为非部分子路径添加成本。 */
		fc_apath->path.total_cost +=
			fc_append_nonpartial_cost(fc_apath->subpaths,
								   fc_apath->first_partial_path,
								   fc_apath->path.parallel_workers);
	}

	/*
	 * 尽管追加不做任何选择或投影，但它并不是免费的；
	 * 添加每个元组的小额开销。
	 */
	fc_apath->path.total_cost +=
		cpu_tuple_cost * APPEND_CPU_COST_MULTIPLIER * fc_apath->path.rows;
}

/*
 * cost_merge_append
 *	  确定并返回MergeAppend节点的成本。
 *
 * MergeAppend合并几个预排序的输入流，使用一个堆，
 * 在任何给定时刻持有来自每个流的下一个元组。如果有N个流，我们需要大约
 * N*log2(N)的元组比较来构建启动时的堆，然后对于每个输出元组，
 * 约需log2(N)次比较来替换顶部条目。
 *
 * (有效的N值将在一些输入流耗尽后下降，
 * 但似乎不值得尝试计算这个。)
 *
 * 堆从不溢出到磁盘，因为我们假设N不是很大。
 * 因此，这比cost_sort简单得多。
 *
 * 像在cost_sort中，我们为每个元组比较收取两个操作符评估费用。
 *
 * 'pathkeys'是排序键的列表
 * 'n_streams'是输入流的数量
 * 'input_startup_cost'是输入流的启动成本之和
 * 'input_total_cost'是输入流的总成本之和
 * 'tuples'是所有流中元组的数量
 */
void cost_merge_append(Path *fc_path, PlannerInfo *fc_root,
				  List *fc_pathkeys, int fc_n_streams,
				  Cost fc_input_startup_cost, Cost fc_input_total_cost,
				  double fc_tuples)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	Cost		fc_comparison_cost;
	double		fc_N;
	double		fc_logN;

	/*
	 * 避免log(0)...
	 */
	fc_N = (fc_n_streams < 2) ? 2.0 : (double) fc_n_streams;
	fc_logN = LOG2(fc_N);

	/* 每次元组比较的假定成本 */
	fc_comparison_cost = 2.0 * cpu_operator_cost;

	/* 堆创建成本 */
	fc_startup_cost += fc_comparison_cost * fc_N * fc_logN;

	/* 每元组堆维护成本 */
	fc_run_cost += fc_tuples * fc_comparison_cost * fc_logN;

	/*
	 * 尽管MergeAppend不做任何选择或投影，但它并不是免费的；
	 * 添加每个元组的小额开销。
	 */
	fc_run_cost += cpu_tuple_cost * APPEND_CPU_COST_MULTIPLIER * fc_tuples;

	fc_path->startup_cost = fc_startup_cost + fc_input_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost + fc_input_total_cost;
}

/*
 * cost_material
 *	  确定并返回物化一个关系的成本，包括
 *	  读取输入数据的成本。
 *
 * 如果要物化的数据总量超过work_mem，
 * 我们将需要将其写入磁盘，因此在这种情况下成本要高得多。
 *
 * 请注意，这里我们是在估算第一次扫描关系的成本，
 * 因此物化全部是开销 --- 节省仅在重新扫描时出现，
 * 重新扫描的成本在cost_rescan中估算。
 */
void cost_material(Path *fc_path,
			  Cost fc_input_startup_cost, Cost fc_input_total_cost,
			  double fc_tuples, int fc_width)
{
	Cost		fc_startup_cost = fc_input_startup_cost;
	Cost		fc_run_cost = fc_input_total_cost - fc_input_startup_cost;
	double		fc_nbytes = fc_relation_byte_size(fc_tuples, fc_width);
	long		fc_work_mem_bytes = work_mem * 1024L;

	fc_path->rows = fc_tuples;

	/*
	 * 无论是否溢出，针对每个元组收取2倍的cpu_operator_cost
	 * 以反映账务开销。 （这个费率必须超过cost_rescan对物化的费用，
	 * 即每个元组的cpu_operator_cost；如果正好相同，那么
	 * 在A外部、物化B内部的 nestloop 和 在B外部、物化A内部的nestloop之间
	 * 将存在成本平衡。额外的成本确保我们会优先考虑物化较小的关系。）
	 * 请注意，这通常比cpu_tuple_cost要低得多；这没问题，因为物化计划节点
	 * 不做条件检查或投影，因此开销低于大多数计划节点。
	 */
	fc_run_cost += 2 * cpu_operator_cost * fc_tuples;

	/*
	 * 如果我们将溢出到磁盘，则按每页seq_page_cost的费率收取费用。
	 * 这个成本假设在计划运行阶段均匀分布，
	 * 虽然不完全准确，但我们的成本模型不允许运行阶段内部的非均匀成本。
	 */
	if (fc_nbytes > fc_work_mem_bytes)
	{
		double		fc_npages = ceil(fc_nbytes / BLCKSZ);

		fc_run_cost += seq_page_cost * fc_npages;
	}

	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * cost_memoize_rescan
 *	  确定重新扫描Memoize节点的估计成本。
 *
 * 为了估算这个，我们必须了解我们期望被调用的频率以及
 * 我们可能被调用的参数集的不同数量。如果我们期望良好的缓存命中率，
 * 那么我们可以设置我们的成本以考虑到该命中率，以及为
 * 缓存本身支付一点成本。如果我们期望使用太多不同的参数值被调用，
 * 缓存将不会很好地工作。这里的最坏情况是我们从未见到
 * 任何参数值两次，在这种情况下我们将永远不会得到缓存命中，
 * 并且缓存将完全浪费努力。
 */
static void fc_cost_memoize_rescan(PlannerInfo *fc_root, MemoizePath *fc_mpath,
					Cost *fc_rescan_startup_cost, Cost *fc_rescan_total_cost)
{
	EstimationInfo fc_estinfo;
	Cost		fc_input_startup_cost = fc_mpath->subpath->startup_cost;
	Cost		fc_input_total_cost = fc_mpath->subpath->total_cost;
	double		fc_tuples = fc_mpath->subpath->rows;
	double		fc_calls = fc_mpath->calls;
	int			fc_width = fc_mpath->subpath->pathtarget->width;

	double		fc_hash_mem_bytes;
	double		fc_est_entry_bytes;
	double		fc_est_cache_entries;
	double		fc_ndistinct;
	double		fc_evict_ratio;
	double		fc_hit_ratio;
	Cost		fc_startup_cost;
	Cost		fc_total_cost;

	/* 可用的缓存空间 */
	fc_hash_mem_bytes = get_hash_memory_limit();

	
/*
	 * 设置每个缓存条目在缓存中应消耗的字节数。
	 * 为了更好地估算我们可以同时存储多少个缓存条目，我们在这里调用执行器，
	 * 询问单个缓存条目的内存开销。
	 *
	 * XXX 我们还存储了缓存键，但这里没有考虑到。
	 */
	fc_est_entry_bytes = fc_relation_byte_size(fc_tuples, fc_width) +
		ExecEstimateCacheEntryOverheadBytes(fc_tuples);

	/* 估算我们可以同时容纳的缓存条目的上限 */
	fc_est_cache_entries = floor(fc_hash_mem_bytes / fc_est_entry_bytes);

	/* 估算参数值的不同数量 */
	fc_ndistinct = estimate_num_groups(fc_root, fc_mpath->param_exprs, fc_calls, NULL,
									&fc_estinfo);

	/*
	 * 当估算回退到使用默认值时，假设使用 Memoize 节点是可以的风险有点大。
	 * 使用默认值可能导致我们在不适合的情况下使用 Memoize 节点。
	 * 如果我们看到已经这样做了，那么我们将假设每个调用都有唯一的参数，
	 * 这几乎肯定意味着一个 MemoizePath 永远不会在 add_path() 中存活。
	 */
	if ((fc_estinfo.flags & SELFLAG_USED_DEFAULT) != 0)
		fc_ndistinct = fc_calls;

	/*
	 * 由于我们已经估算了可以一次存储的最大条目数，并且知道估算的
	 * 不同值的数量，我们将借此机会设置路径的 est_entries。
	 * 这将最终决定执行器将使用的哈希表大小。如果我们将其保持为零，
	 * 执行器将自行选择大小。实际上，这不是进行此操作的合适地方，
	 * 但由于一切已经计算完毕，便利性使我们这样做。
	 */
	fc_mpath->est_entries = Min(Min(fc_ndistinct, fc_est_cache_entries),
							 PG_UINT32_MAX);

	/*
	 * 当不同参数值的数量超过我们可以在缓存中存储的数量时，
	 * 我们将不得不从缓存中逐出一些条目。这不是免费的。
	 * 在这里我们估算我们将多少次承担该驱逐的成本。
	 */
	fc_evict_ratio = 1.0 - Min(fc_est_cache_entries, fc_ndistinct) / fc_ndistinct;

	/*
	 * 为了估算单次扫描的成本，我们需要尝试估算缓存命中率。
	 * 为此，我们必须查看此节点预计的总扫描次数以及我们期望
	 * 获取缓存命中的扫描次数。
	 */
	fc_hit_ratio = 1.0 / fc_ndistinct * Min(fc_est_cache_entries, fc_ndistinct) -
		(fc_ndistinct / fc_calls);

	/* 确保我们不会变成负数 */
	fc_hit_ratio = Max(fc_hit_ratio, 0.0);

	/*
	 * 设置 total_cost，考虑到预期的缓存命中率。
	 * 我们还添加了 cpu_operator_cost 来考虑缓存查找。
	 * 无论命中与否，这都会发生。
	 */
	fc_total_cost = fc_input_total_cost * (1.0 - fc_hit_ratio) + cpu_operator_cost;

	/* 现在调整总成本以考虑缓存驱逐 */

	/* 收取驱逐实际缓存条目的 cpu_tuple_cost */
	fc_total_cost += cpu_tuple_cost * fc_evict_ratio;

	/*
	 * 对于每个元组驱逐，收取 1/10 的 cpu_operator_cost。
	 * 每个元组的驱逐实际上只是一个 pfree，因此收取整个
	 * cpu_operator_cost 似乎有点过于高昂。
	 */
	fc_total_cost += cpu_operator_cost / 10.0 * fc_evict_ratio * fc_tuples;

	/*
	 * 现在要调整存储在缓存中的成本，因为这也不是免费的。
	 * 一切必须放入缓存。我们不会按任何比例来分配这个
	 * 成本，只是在扫描时应用一次。我们为创建缓存条目收取
	 * cpu_tuple_cost，并且对于我们期望缓存的每个元组收取
	 * cpu_operator_cost。
	 */
	fc_total_cost += cpu_tuple_cost + cpu_operator_cost * fc_tuples;

	/*
	 * 获取第一行的成本必须根据预期的缓存命中率进行调整。
	 */
	fc_startup_cost = fc_input_startup_cost * (1.0 - fc_hit_ratio);

	/*
	 * 此外，我们收取一个 cpu_tuple_cost 来计算缓存查找的成本，
	 * 不论是缓存命中与否，我们都会这样做。
	 */
	fc_startup_cost += cpu_tuple_cost;

	*fc_rescan_startup_cost = fc_startup_cost;
	*fc_rescan_total_cost = fc_total_cost;
}

/*
 * cost_agg
 *		确定并返回执行 Agg 计划节点的成本，
 *		包括其输入的成本。
 *
 * 当没有实际的聚合函数时（也就是说，
 * 我们仅使用哈希 Agg 节点进行分组），aggcosts 可以为 NULL。
 *
 * 注：当 aggstrategy == AGG_SORTED 时，调用方必须确保输入成本
 * 适用于适当地排序的输入。
 */
void cost_agg(Path *fc_path, PlannerInfo *fc_root,
		 AggStrategy fc_aggstrategy, const AggClauseCosts *fc_aggcosts,
		 int fc_numGroupCols, double fc_numGroups,
		 List *fc_quals,
		 Cost fc_input_startup_cost, Cost fc_input_total_cost,
		 double fc_input_tuples, double fc_input_width)
{
	double		fc_output_tuples;
	Cost		fc_startup_cost;
	Cost		fc_total_cost;
	AggClauseCosts fc_dummy_aggcosts;

	/* 如果传入 NULL，则使用全零的每个聚合成本 */
	if (fc_aggcosts == NULL)
	{
		Assert(fc_aggstrategy == AGG_HASHED);
		MemSet(&fc_dummy_aggcosts, 0, sizeof(AggClauseCosts));
		fc_aggcosts = &fc_dummy_aggcosts;
	}

	/*
	 * aggcosts 的 transCost.per_tuple 组件应当在每个输入元组上收取一次，
	 * 对应于评估聚合 transfns 及其输入表达式的成本。finalCost.per_tuple
	 * 组件在每个输出元组上收取一次，对应于评估 finalfns 的成本。
	 * 启动成本当然只收取一次。
	 *
	 * 如果我们正在进行分组，则每个输入元组对于分组比较收取
	 * 额外的 cpu_operator_cost。
	 *
	 * 如果不进行分组，我们将产生一个单独的输出元组，
	 * 否则每个组产生一个元组。我们为每个输出元组收取 cpu_tuple_cost。
	 *
	 * 注：在这个成本模型中，AGG_SORTED 和 AGG_HASHED 具有完全相同的
	 * 总 CPU 成本，但 AGG_SORTED 的启动成本更低。如果输入路径已经
	 * 适当地被排序，应该优先选择 AGG_SORTED（因为没有内存溢出的风险）。
	 * 只要计算出的总成本确实完全相等，这种情况就会发生---
	 * 但是如果有舍入误差，我们可能会做出错误的决定。因此，请确保
	 * 以下计算以相同的顺序形成相同的中间值。
	 */
	if (fc_aggstrategy == AGG_PLAIN)
	{
		fc_startup_cost = fc_input_total_cost;
		fc_startup_cost += fc_aggcosts->transCost.startup;
		fc_startup_cost += fc_aggcosts->transCost.per_tuple * fc_input_tuples;
		fc_startup_cost += fc_aggcosts->finalCost.startup;
		fc_startup_cost += fc_aggcosts->finalCost.per_tuple;
		/* 我们没有进行分组 */
		fc_total_cost = fc_startup_cost + cpu_tuple_cost;
		fc_output_tuples = 1;
	}
	else if (fc_aggstrategy == AGG_SORTED || fc_aggstrategy == AGG_MIXED)
	{
		/* 在这里我们能够即时交付输出 */
		fc_startup_cost = fc_input_startup_cost;
		fc_total_cost = fc_input_total_cost;
		if (fc_aggstrategy == AGG_MIXED && !enable_hashagg)
		{
			fc_startup_cost += disable_cost;
			fc_total_cost += disable_cost;
		}
		/* 这样表述的计算与 HASHED 情况相吻合，见上文注释 */
		fc_total_cost += fc_aggcosts->transCost.startup;
		fc_total_cost += fc_aggcosts->transCost.per_tuple * fc_input_tuples;
		fc_total_cost += (cpu_operator_cost * fc_numGroupCols) * fc_input_tuples;
		fc_total_cost += fc_aggcosts->finalCost.startup;
		fc_total_cost += fc_aggcosts->finalCost.per_tuple * fc_numGroups;
		fc_total_cost += cpu_tuple_cost * fc_numGroups;
		fc_output_tuples = fc_numGroups;
	}
	else
	{
		/* 必须是 AGG_HASHED */
		fc_startup_cost = fc_input_total_cost;
		if (!enable_hashagg)
			fc_startup_cost += disable_cost;
		fc_startup_cost += fc_aggcosts->transCost.startup;
		fc_startup_cost += fc_aggcosts->transCost.per_tuple * fc_input_tuples;
		/* 计算哈希值的成本 */
		fc_startup_cost += (cpu_operator_cost * fc_numGroupCols) * fc_input_tuples;
		fc_startup_cost += fc_aggcosts->finalCost.startup;

		fc_total_cost = fc_startup_cost;
		fc_total_cost += fc_aggcosts->finalCost.per_tuple * fc_numGroups;
		/* 从哈希表获取的成本 */
		fc_total_cost += cpu_tuple_cost * fc_numGroups;
		fc_output_tuples = fc_numGroups;
	}

	/*
	 * 增加溢出到磁盘的哈希聚合的磁盘成本。
	 *
	 * 进入哈希表的组在最终确定之前保持在内存中，
	 * 因此溢出和重新处理元组不会产生额外的 transCost 或 finalCost 调用。
	 * 此外，计算的哈希值与溢出的元组一起存储，
	 * 因此我们不会产生额外的哈希函数调用。
	 *
	 * 哈希聚合在第一个批次完成后开始返回元组。
	 * 将写入（溢出的元组）累加到 startup_cost 和 total_cost；
	 * 仅将读取累加到 total_cost。
	 */
	if (fc_aggstrategy == AGG_HASHED || fc_aggstrategy == AGG_MIXED)
	{
		double		fc_pages;
		double		fc_pages_written = 0.0;
		double		fc_pages_read = 0.0;
		double		fc_spill_cost;
		double		fc_hashentrysize;
		double		fc_nbatches;
		Size		fc_mem_limit;
		uint64		fc_ngroups_limit;
		int			fc_num_partitions;
		int			fc_depth;

		/*
		 * 根据计算的限制估计批次数。如果小于或等于一，
		 * 所有组预计都能装入内存；否则我们预计会溢出。
		 */
		fc_hashentrysize = hash_agg_entry_size(list_length(fc_root->aggtransinfos),
											fc_input_width,
											fc_aggcosts->transitionSpace);
		hash_agg_set_limits(fc_hashentrysize, fc_numGroups, 0, &fc_mem_limit,
							&fc_ngroups_limit, &fc_num_partitions);

		fc_nbatches = Max((fc_numGroups * fc_hashentrysize) / fc_mem_limit,
					   fc_numGroups / fc_ngroups_limit);

		fc_nbatches = Max(ceil(fc_nbatches), 1.0);
		fc_num_partitions = Max(fc_num_partitions, 2);

		/*
		 * 不同递归层级的分区数量可以变化；
		 * 但就此计算而言，假设它保持不变。
		 */
		fc_depth = ceil(log(fc_nbatches) / log(fc_num_partitions));

		/*
		 * 估计读取和写入的页数。对于每个递归层级，
		 * 必须写入一个元组，然后稍后读取。
		 */
		fc_pages = fc_relation_byte_size(fc_input_tuples, fc_input_width) / BLCKSZ;
		fc_pages_written = fc_pages_read = fc_pages * fc_depth;

		/*
		 * 哈希聚合的 IO 性能在典型的硬件/操作系统组合下
		 * 较排序要差。对这点进行通用的处罚。
		 */
		fc_pages_read *= 2.0;
		fc_pages_written *= 2.0;

		fc_startup_cost += fc_pages_written * random_page_cost;
		fc_total_cost += fc_pages_written * random_page_cost;
		fc_total_cost += fc_pages_read * seq_page_cost;

		/* 计算溢出一个元组和读取回来的 CPU 成本 */
		fc_spill_cost = fc_depth * fc_input_tuples * 2.0 * cpu_tuple_cost;
		fc_startup_cost += fc_spill_cost;
		fc_total_cost += fc_spill_cost;
	}

	/*
	 * 如果有资格（HAVING 资格），则计算其成本和
	 * 选择性。
	 */
	if (fc_quals)
	{
		QualCost	fc_qual_cost;

		cost_qual_eval(&fc_qual_cost, fc_quals, fc_root);
		fc_startup_cost += fc_qual_cost.startup;
		fc_total_cost += fc_qual_cost.startup + fc_output_tuples * fc_qual_cost.per_tuple;

		fc_output_tuples = clamp_row_est(fc_output_tuples *
									  clauselist_selectivity(fc_root,
															 fc_quals,
															 0,
															 JOIN_INNER,
															 NULL));
	}

	fc_path->rows = fc_output_tuples;
	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_total_cost;
}

/*
 * cost_windowagg
 *		确定并返回执行 WindowAgg 计划节点的成本，
 *		包括其输入的成本。
 *
 * 假设输入已经正确排序。
 */
void cost_windowagg(Path *fc_path, PlannerInfo *fc_root,
			   List *fc_windowFuncs, int fc_numPartCols, int fc_numOrderCols,
			   Cost fc_input_startup_cost, Cost fc_input_total_cost,
			   double fc_input_tuples)
{
	Cost		fc_startup_cost;
	Cost		fc_total_cost;
	ListCell   *fc_lc;

	fc_startup_cost = fc_input_startup_cost;
	fc_total_cost = fc_input_total_cost;

	/*
	 * 窗口函数假定每个元组的声明执行成本加上
	 * 评估其输入表达式的成本。由于它们实际上可能在每个周期
	 * 评估多个行，这可能会严重低估成本；但由于无法知道
	 * 窗口函数将提取多少行，因此难以做得更好。
	 * 在任何情况下，对于所有内置窗口函数，这是一个良好的估计，
	 * 所以我们就暂时这样做。
	 */
	foreach(fc_lc, fc_windowFuncs)
	{
		WindowFunc *fc_wfunc = lfirst_node(WindowFunc, fc_lc);
		Cost		fc_wfunccost;
		QualCost	fc_argcosts;

		fc_argcosts.startup = fc_argcosts.per_tuple = 0;
		add_function_cost(fc_root, fc_wfunc->winfnoid, (Node *) fc_wfunc,
						  &fc_argcosts);
		fc_startup_cost += fc_argcosts.startup;
		fc_wfunccost = fc_argcosts.per_tuple;

		/* 还将输入表达式的成本添加到每输入行成本中 */
		cost_qual_eval_node(&fc_argcosts, (Node *) fc_wfunc->args, fc_root);
		fc_startup_cost += fc_argcosts.startup;
		fc_wfunccost += fc_argcosts.per_tuple;

		
/*
		 * 将过滤器的成本添加到每行输入的成本中。 XXX 我们应该根据过滤器的选择性减少输入表达式的成本。
		 */
		cost_qual_eval_node(&fc_argcosts, (Node *) fc_wfunc->aggfilter, fc_root);
		fc_startup_cost += fc_argcosts.startup;
		fc_wfunccost += fc_argcosts.per_tuple;

		fc_total_cost += fc_wfunccost * fc_input_tuples;
	}

	/*
	 * 对每个分组列每个元组收取 cpu_operator_cost 以进行分组比较，对于一般开销每个元组还需加上 cpu_tuple_cost。
	 *
	 * XXX 这忽略了在工作内存溢出时将数据临时存储到磁盘的成本。迟早应该考虑到这一点。
	 */
	fc_total_cost += cpu_operator_cost * (fc_numPartCols + fc_numOrderCols) * fc_input_tuples;
	fc_total_cost += cpu_tuple_cost * fc_input_tuples;

	fc_path->rows = fc_input_tuples;
	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_total_cost;
}

/*
 * cost_group
 *		确定并返回执行 Group 计划节点的成本，包括其输入的成本。
 *
 * 注意：调用者必须确保输入成本适用于适当排序的输入。
 */
void cost_group(Path *fc_path, PlannerInfo *fc_root,
		   int fc_numGroupCols, double fc_numGroups,
		   List *fc_quals,
		   Cost fc_input_startup_cost, Cost fc_input_total_cost,
		   double fc_input_tuples)
{
	double		fc_output_tuples;
	Cost		fc_startup_cost;
	Cost		fc_total_cost;

	fc_output_tuples = fc_numGroups;
	fc_startup_cost = fc_input_startup_cost;
	fc_total_cost = fc_input_total_cost;

	/*
	 * 对每个输入元组的每次比较收取一个 cpu_operator_cost。我们假设所有列在大多数元组中都进行比较。
	 */
	fc_total_cost += cpu_operator_cost * fc_input_tuples * fc_numGroupCols;

	/*
	 * 如果有资格（HAVING 资格），则计算其成本和
	 * 选择性。
	 */
	if (fc_quals)
	{
		QualCost	fc_qual_cost;

		cost_qual_eval(&fc_qual_cost, fc_quals, fc_root);
		fc_startup_cost += fc_qual_cost.startup;
		fc_total_cost += fc_qual_cost.startup + fc_output_tuples * fc_qual_cost.per_tuple;

		fc_output_tuples = clamp_row_est(fc_output_tuples *
									  clauselist_selectivity(fc_root,
															 fc_quals,
															 0,
															 JOIN_INNER,
															 NULL));
	}

	fc_path->rows = fc_output_tuples;
	fc_path->startup_cost = fc_startup_cost;
	fc_path->total_cost = fc_total_cost;
}

/*
 * initial_cost_nestloop
 *	  嵌套循环连接路径成本的初步估计。
 *
 * 这必须快速生成路径启动和总成本的下限估计。如果我们无法使用下限消除提议的路径，那么将调用 final_cost_nestloop 以获得最终估计。
 *
 * 此函数与 final_cost_nestloop 之间的确切分工是私有的，它代表了初步估计速度与获得紧凑下限之间的权衡。我们选择不在这里检查连接条件，因为这显然是计算中成本最高的部分。最终结果是 CPU 成本的考虑必须留到第二阶段；对于 SEMI/ANTI 连接，我们还必须推迟考虑内部路径的运行成本。
 *
 * 'workspace' 用于填充启动成本、总成本，以及最终_cost_nestloop 可能使用的其他数据。
 * 'jointype' 是要执行的连接类型。
 * 'outer_path' 是连接的外部输入。
 * 'inner_path' 是连接的内部输入。
 * 'extra' 包含有关连接的其他信息。
 */
void initial_cost_nestloop(PlannerInfo *fc_root, JoinCostWorkspace *fc_workspace,
					  JoinType fc_jointype,
					  Path *fc_outer_path, Path *fc_inner_path,
					  JoinPathExtraData *fc_extra)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	double		fc_outer_path_rows = fc_outer_path->rows;
	Cost		fc_inner_rescan_start_cost;
	Cost		fc_inner_rescan_total_cost;
	Cost		fc_inner_run_cost;
	Cost		fc_inner_rescan_run_cost;

	/* 估算重新扫描内部关系的成本 */
	fc_cost_rescan(fc_root, fc_inner_path,
				&fc_inner_rescan_start_cost,
				&fc_inner_rescan_total_cost);

	/* 源数据的成本 */

	/*
	 * 注意：显然，我们必须支付外部和内部路径的启动成本
	 * 才能开始返回元组，因此连接的启动成本是
	 * 它们的总和。我们还将多次支付内部路径的重扫描启动成本。
	 */
	fc_startup_cost += fc_outer_path->startup_cost + fc_inner_path->startup_cost;
	fc_run_cost += fc_outer_path->total_cost - fc_outer_path->startup_cost;
	if (fc_outer_path_rows > 1)
		fc_run_cost += (fc_outer_path_rows - 1) * fc_inner_rescan_start_cost;

	fc_inner_run_cost = fc_inner_path->total_cost - fc_inner_path->startup_cost;
	fc_inner_rescan_run_cost = fc_inner_rescan_total_cost - fc_inner_rescan_start_cost;

	if (fc_jointype == JOIN_SEMI || fc_jointype == JOIN_ANTI ||
		fc_extra->inner_unique)
	{
		/*
		 * 对于SEMI或ANTI连接，或如果内部关系已知唯一，执行器
		 * 在第一次匹配后将停止。
		 *
		 * 获取合理的估计需要检查连接条件，
		 * 我们选择将其推迟到final_cost_nestloop。
		 */

		/* 为final_cost_nestloop保存私有数据 */
		fc_workspace->inner_run_cost = fc_inner_run_cost;
		fc_workspace->inner_rescan_run_cost = fc_inner_rescan_run_cost;
	}
	else
	{
		/* 正常情况；我们将为每一行外部数据扫描整个输入关系 */
		fc_run_cost += fc_inner_run_cost;
		if (fc_outer_path_rows > 1)
			fc_run_cost += (fc_outer_path_rows - 1) * fc_inner_rescan_run_cost;
	}

	/* CPU成本留待后续处理 */

	/* 公共结果字段 */
	fc_workspace->startup_cost = fc_startup_cost;
	fc_workspace->total_cost = fc_startup_cost + fc_run_cost;
	/* 为final_cost_nestloop保存私有数据 */
	fc_workspace->run_cost = fc_run_cost;
}

/*
 * final_cost_nestloop
 *	  嵌套循环连接路径的最终成本和结果大小估计。
 *
 * 'path' 除了行和成本字段外已经填充完成
 * 'workspace' 是来自initial_cost_nestloop的结果
 * 'extra' 包含有关连接的杂项信息
 */
void final_cost_nestloop(PlannerInfo *fc_root, NestPath *fc_path,
					JoinCostWorkspace *fc_workspace,
					JoinPathExtraData *fc_extra)
{
	Path	   *fc_outer_path = fc_path->jpath.outerjoinpath;
	Path	   *fc_inner_path = fc_path->jpath.innerjoinpath;
	double		fc_outer_path_rows = fc_outer_path->rows;
	double		fc_inner_path_rows = fc_inner_path->rows;
	Cost		fc_startup_cost = fc_workspace->startup_cost;
	Cost		fc_run_cost = fc_workspace->run_cost;
	Cost		fc_cpu_per_tuple;
	QualCost	fc_restrict_qual_cost;
	double		fc_ntuples;

	/* 保护下面的假设，即行数不是零 */
	if (fc_outer_path_rows <= 0)
		fc_outer_path_rows = 1;
	if (fc_inner_path_rows <= 0)
		fc_inner_path_rows = 1;
	/* 使用正确的行估计标记路径 */
	if (fc_path->jpath.path.param_info)
		fc_path->jpath.path.rows = fc_path->jpath.path.param_info->ppi_rows;
	else
		fc_path->jpath.path.rows = fc_path->jpath.path.parent->rows;

	/* 对于部分路径，缩放行估计。 */
	if (fc_path->jpath.path.parallel_workers > 0)
	{
		double		fc_parallel_divisor = get_parallel_divisor(&fc_path->jpath.path);

		fc_path->jpath.path.rows =
			clamp_row_est(fc_path->jpath.path.rows / fc_parallel_divisor);
	}

	/*
	 * 我们可以在初步估计中包括disable_cost，但那
	 * 将等同于优化连接方法禁用的情况，这似乎不是赌注的方向。
	 */
	if (!enable_nestloop)
		fc_startup_cost += disable_cost;

	/* 内部关系源数据的成本（我们已经处理了外部关系） */

	if (fc_path->jpath.jointype == JOIN_SEMI || fc_path->jpath.jointype == JOIN_ANTI ||
		fc_extra->inner_unique)
	{
		/*
		 * 对于SEMI或ANTI连接，或如果内部关系已知唯一，执行器
		 * 在第一次匹配后将停止。
		 */
		Cost		fc_inner_run_cost = fc_workspace->inner_run_cost;
		Cost		fc_inner_rescan_run_cost = fc_workspace->inner_rescan_run_cost;
		double		fc_outer_matched_rows;
		double		fc_outer_unmatched_rows;
		Selectivity fc_inner_scan_frac;

		/*
		 * 对于至少有一次匹配的外部关系行，我们可以预期
		 * 内部扫描将在内匹配行数的1/(match_count+1)的分数后停止，
		 * 前提是匹配是均匀分布的。由于它们可能
		 * 并不完全均匀分布，我们对该分数应用了2.0的模糊因子。
		 * （如果我们使用更大的模糊因子，我们将必须将
		 * inner_scan_frac限制为最多1.0；但由于match_count至少为1，
		 * 现在不需要这样的限制。）
		 */
		fc_outer_matched_rows = rint(fc_outer_path_rows * fc_extra->semifactors.outer_match_frac);
		fc_outer_unmatched_rows = fc_outer_path_rows - fc_outer_matched_rows;
		fc_inner_scan_frac = 2.0 / (fc_extra->semifactors.match_count + 1.0);

		/*
		 * 计算处理的元组数量（而不是发出的数量！）。首先，
		 * 考虑成功匹配的外部行。
		 */
		fc_ntuples = fc_outer_matched_rows * fc_inner_path_rows * fc_inner_scan_frac;

		/*
		 * 现在我们需要估计扫描内部关系的实际成本，
		 * 由于提前停止扫描，可能远低于N倍的inner_run_cost。
		 * 我们考虑两种情况。如果内部路径
		 * 是使用所有连接条件作为索引条件的索引扫描，那么一个
		 * 未匹配的外部行将导致索引扫描不返回任何行，
		 * 这可能是相当便宜的。否则，执行器将必须
		 * 扫描整个内部关系以找到未匹配的行；这不太便宜。
		 */
		if (fc_has_indexed_join_quals(fc_path))
		{
			/*
			 * 成功匹配的外部行只需要扫描
			 * inner_scan_frac的内部关系。在这种情况下，我们不
			 * 需要收取全部的inner_run_cost，即使那超过了
			 * inner_rescan_run_cost，因为我们可以假设没有
			 * 内部扫描会扫描整个内部关系。因此假设所有内部扫描
			 * 执行可以是全部成本的分数是可以的，即使物化减少了
			 * 重扫描成本。在写作时，对于物化内部扫描，无法到达
			 * 此处，所以inner_run_cost和
			 * inner_rescan_run_cost反正是相同的；但以防万一，
			 * 对于第一个匹配的元组使用inner_run_cost，
			 * 对于额外的元组使用inner_rescan_run_cost。
			 */
			fc_run_cost += fc_inner_run_cost * fc_inner_scan_frac;
			if (fc_outer_matched_rows > 1)
				fc_run_cost += (fc_outer_matched_rows - 1) * fc_inner_rescan_run_cost * fc_inner_scan_frac;

			/*
			 * 为未匹配的外部行添加内部扫描执行的成本。
			 * 我们估计这与返回非空扫描的第一个元组的成本相同。
			 * 我们认为这些都是重扫描，
			 * 因为我们已经使用过inner_run_cost一次。
			 */
			fc_run_cost += fc_outer_unmatched_rows *
				fc_inner_rescan_run_cost / fc_inner_path_rows;

			/*
			 * 我们不会对未匹配的行评估任何条件，因此
			 * 不要将它们添加到ntuples中。
			 */
		}
		else
		{
			/*
			 * 在这里，一个复杂因素是重扫描可能比
			 * 第一次扫描更便宜。如果我们从未扫描到
			 * 内部关系的末尾，可能（取决于计划类型）我们永远
			 * 不会支付完整的内部第一次扫描运行成本。但是很难
			 * 估计这是否会发生（如果有任何未匹配的外部行，可能
			 * 不会发生！），所以要保守，总是一次性收取
			 * 完整的第一次扫描成本。我们认为这个费用是对应于
			 * 第一个未匹配的外部行，除非我们的估计中没有
			 * 这样的行，在这种情况下将其归咎于第一个匹配的行。
			 */

			/* 首先，将所有未匹配的连接元组计为正在处理 */
			fc_ntuples += fc_outer_unmatched_rows * fc_inner_path_rows;

			/* 现在添加强制的全扫描，并减少适当计数 */
			fc_run_cost += fc_inner_run_cost;
			if (fc_outer_unmatched_rows >= 1)
				fc_outer_unmatched_rows -= 1;
			else
				fc_outer_matched_rows -= 1;

			/* 为具有匹配的额外外部元组添加内部运行成本 */
			if (fc_outer_matched_rows > 0)
				fc_run_cost += fc_outer_matched_rows * fc_inner_rescan_run_cost * fc_inner_scan_frac;

			/* 为额外的未匹配外部元组添加内部运行成本 */
			if (fc_outer_unmatched_rows > 0)
				fc_run_cost += fc_outer_unmatched_rows * fc_inner_rescan_run_cost;
		}
	}
	else
	{
		
/* 正常情况的源成本已包含在初步估算中 */

		/* 计算处理的元组数量（而不是发出的数量！） */
		fc_ntuples = fc_outer_path_rows * fc_inner_path_rows;
	}

	/* CPU成本 */
	cost_qual_eval(&fc_restrict_qual_cost, fc_path->jpath.joinrestrictinfo, fc_root);
	fc_startup_cost += fc_restrict_qual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_restrict_qual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_ntuples;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->jpath.path.pathtarget->cost.startup;
	fc_run_cost += fc_path->jpath.path.pathtarget->cost.per_tuple * fc_path->jpath.path.rows;

	fc_path->jpath.path.startup_cost = fc_startup_cost;
	fc_path->jpath.path.total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * initial_cost_mergejoin
 *	  合并连接路径成本的初步估算。
 *
 * 这必须快速生成路径启动和总成本的下限估算。
 * 如果我们无法利用下限排除提议的路径，将调用 final_cost_mergejoin
 * 以获得最终估算。
 *
 * 此函数和 final_cost_mergejoin 之间的确切分工是它们的私有内容，
 * 代表了初步估算速度与获取紧密下限之间的权衡。我们选择不在这里检查
 * 连接条件，除了获取扫描选择性估算，这真的很重要（但幸运的是，
 * 使用缓存使得获得这个估算的成本保持在合理范围内）。
 * 我们还假设 cost_sort 在这里使用起来足够便宜。
 *
 * 'workspace' 应填充启动成本、总成本，以及最终_cost_mergejoin 可能使用的
 * 其他数据
 * 'jointype' 是要执行的连接类型
 * 'mergeclauses' 是作为合并子句将要使用的连接子句列表
 * 'outer_path' 是连接的外部输入
 * 'inner_path' 是连接的内部输入
 * 'outersortkeys' 是外部路径的排序键列表
 * 'innersortkeys' 是内部路径的排序键列表
 * 'extra' 包含有关连接的杂项信息
 *
 * 注意：如果不需要显式排序，则 outersortkeys 和 innersortkeys 应为 NIL，
 * 因为相应的源路径已排序。
 */
void initial_cost_mergejoin(PlannerInfo *fc_root, JoinCostWorkspace *fc_workspace,
					   JoinType fc_jointype,
					   List *fc_mergeclauses,
					   Path *fc_outer_path, Path *fc_inner_path,
					   List *fc_outersortkeys, List *fc_innersortkeys,
					   JoinPathExtraData *fc_extra)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	double		fc_outer_path_rows = fc_outer_path->rows;
	double		fc_inner_path_rows = fc_inner_path->rows;
	Cost		fc_inner_run_cost;
	double		fc_outer_rows,
				fc_inner_rows,
				fc_outer_skip_rows,
				fc_inner_skip_rows;
	Selectivity fc_outerstartsel,
				fc_outerendsel,
				fc_innerstartsel,
				fc_innerendsel;
	Path		sort_path;		/* cost_sort 结果的占位符 */

	/* 保护下面的假设，即行数不是零 */
	if (fc_outer_path_rows <= 0)
		fc_outer_path_rows = 1;
	if (fc_inner_path_rows <= 0)
		fc_inner_path_rows = 1;

	/*
	 * 合并连接将会在耗尽任一输入流后停止
	 * （除非它是外连接，在这种情况下，外侧必须完全扫描）。
	 * 估算左侧和右侧输入的实际需要扫描的部分。
	 * 同样，我们可以估算在找到第一个连接对之前会跳过的行数，
	 * 这应该被纳入启动成本。我们仅使用第一个（最重要的）合并子句
	 * 来达到这个目的。由于 mergejoinscansel() 是一个相当昂贵的计算，
	 * 我们在合并子句 RestrictInfo 中缓存结果。
	 */
	if (fc_mergeclauses && fc_jointype != JOIN_FULL)
	{
		RestrictInfo *fc_firstclause = (RestrictInfo *) linitial(fc_mergeclauses);
		List	   *fc_opathkeys;
		List	   *fc_ipathkeys;
		PathKey    *fc_opathkey;
		PathKey    *fc_ipathkey;
		MergeScanSelCache *fc_cache;

		/* 获取输入路径键以确定排序顺序详情 */
		fc_opathkeys = fc_outersortkeys ? fc_outersortkeys : fc_outer_path->pathkeys;
		fc_ipathkeys = fc_innersortkeys ? fc_innersortkeys : fc_inner_path->pathkeys;
		Assert(fc_opathkeys);
		Assert(fc_ipathkeys);
		fc_opathkey = (PathKey *) linitial(fc_opathkeys);
		fc_ipathkey = (PathKey *) linitial(fc_ipathkeys);
		/* 调试检查 */
		if (fc_opathkey->pk_opfamily != fc_ipathkey->pk_opfamily ||
			fc_opathkey->pk_eclass->ec_collation != fc_ipathkey->pk_eclass->ec_collation ||
			fc_opathkey->pk_strategy != fc_ipathkey->pk_strategy ||
			fc_opathkey->pk_nulls_first != fc_ipathkey->pk_nulls_first)
			elog(ERROR, "left and right pathkeys do not match in mergejoin");

		/* 获取带缓存的选择性 */
		fc_cache = fc_cached_scansel(fc_root, fc_firstclause, fc_opathkey);

		if (bms_is_subset(fc_firstclause->left_relids,
						  fc_outer_path->parent->relids))
		{
			/* 子句的左侧是外部 */
			fc_outerstartsel = fc_cache->leftstartsel;
			fc_outerendsel = fc_cache->leftendsel;
			fc_innerstartsel = fc_cache->rightstartsel;
			fc_innerendsel = fc_cache->rightendsel;
		}
		else
		{
			/* 子句的左侧是内部 */
			fc_outerstartsel = fc_cache->rightstartsel;
			fc_outerendsel = fc_cache->rightendsel;
			fc_innerstartsel = fc_cache->leftstartsel;
			fc_innerendsel = fc_cache->leftendsel;
		}
		if (fc_jointype == JOIN_LEFT ||
			fc_jointype == JOIN_ANTI)
		{
			fc_outerstartsel = 0.0;
			fc_outerendsel = 1.0;
		}
		else if (fc_jointype == JOIN_RIGHT)
		{
			fc_innerstartsel = 0.0;
			fc_innerendsel = 1.0;
		}
	}
	else
	{
		/* 处理无子句或全连接的合并连接 */
		fc_outerstartsel = fc_innerstartsel = 0.0;
		fc_outerendsel = fc_innerendsel = 1.0;
	}

	/*
	 * 将选择性转换为行数。我们强制 outer_rows 和
	 * inner_rows 至少为 1，但 skip_rows 的估计可以为零。
	 */
	fc_outer_skip_rows = rint(fc_outer_path_rows * fc_outerstartsel);
	fc_inner_skip_rows = rint(fc_inner_path_rows * fc_innerstartsel);
	fc_outer_rows = clamp_row_est(fc_outer_path_rows * fc_outerendsel);
	fc_inner_rows = clamp_row_est(fc_inner_path_rows * fc_innerendsel);

	Assert(fc_outer_skip_rows <= fc_outer_rows);
	Assert(fc_inner_skip_rows <= fc_inner_rows);

	/*
	 * 重新调整扫描选择性以考虑上述取整。这通常是一个微不足道的影响，
	 * 但当输入中只有少数行时，未能这样做会导致较大的百分比误差。
	 */
	fc_outerstartsel = fc_outer_skip_rows / fc_outer_path_rows;
	fc_innerstartsel = fc_inner_skip_rows / fc_inner_path_rows;
	fc_outerendsel = fc_outer_rows / fc_outer_path_rows;
	fc_innerendsel = fc_inner_rows / fc_inner_path_rows;

	Assert(fc_outerstartsel <= fc_outerendsel);
	Assert(fc_innerstartsel <= fc_innerendsel);

	/* 源数据的成本 */

	if (fc_outersortkeys)			/* 我们需要对外部进行排序吗？ */
	{
		cost_sort(&sort_path,
				  fc_root,
				  fc_outersortkeys,
				  fc_outer_path->total_cost,
				  fc_outer_path_rows,
				  fc_outer_path->pathtarget->width,
				  0.0,
				  work_mem,
				  -1.0);
		fc_startup_cost += sort_path.startup_cost;
		fc_startup_cost += (sort_path.total_cost - sort_path.startup_cost)
			* fc_outerstartsel;
		fc_run_cost += (sort_path.total_cost - sort_path.startup_cost)
			* (fc_outerendsel - fc_outerstartsel);
	}
	else
	{
		fc_startup_cost += fc_outer_path->startup_cost;
		fc_startup_cost += (fc_outer_path->total_cost - fc_outer_path->startup_cost)
			* fc_outerstartsel;
		fc_run_cost += (fc_outer_path->total_cost - fc_outer_path->startup_cost)
			* (fc_outerendsel - fc_outerstartsel);
	}

	if (fc_innersortkeys)			/* 我们需要对内部进行排序吗？ */
	{
		cost_sort(&sort_path,
				  fc_root,
				  fc_innersortkeys,
				  fc_inner_path->total_cost,
				  fc_inner_path_rows,
				  fc_inner_path->pathtarget->width,
				  0.0,
				  work_mem,
				  -1.0);
		fc_startup_cost += sort_path.startup_cost;
		fc_startup_cost += (sort_path.total_cost - sort_path.startup_cost)
			* fc_innerstartsel;
		fc_inner_run_cost = (sort_path.total_cost - sort_path.startup_cost)
			* (fc_innerendsel - fc_innerstartsel);
	}
	else
	{
		fc_startup_cost += fc_inner_path->startup_cost;
		fc_startup_cost += (fc_inner_path->total_cost - fc_inner_path->startup_cost)
			* fc_innerstartsel;
		fc_inner_run_cost = (fc_inner_path->total_cost - fc_inner_path->startup_cost)
			* (fc_innerendsel - fc_innerstartsel);
	}

	/*
	 * 我们无法确定是否会发生重新扫描，或者内部输入是否应进行材料化。
	 * 无论重新扫描和材料化的考虑如何，最小的内部输入成本是 inner_run_cost。
	 * 我们将其包含在 workspace->total_cost 中，但尚未包含在 run_cost 中。
	 */

	/* CPU成本留待后续处理 */

	/* 公共结果字段 */
	fc_workspace->startup_cost = fc_startup_cost;
	fc_workspace->total_cost = fc_startup_cost + fc_run_cost + fc_inner_run_cost;
	/* 保存最终成本合并连接的私有数据 */
	fc_workspace->run_cost = fc_run_cost;
	fc_workspace->inner_run_cost = fc_inner_run_cost;
	fc_workspace->outer_rows = fc_outer_rows;
	fc_workspace->inner_rows = fc_inner_rows;
	fc_workspace->outer_skip_rows = fc_outer_skip_rows;
	fc_workspace->inner_skip_rows = fc_inner_skip_rows;
}

/*
 * final_cost_mergejoin
 *	  合并路径的成本和结果大小的最终估计。
 *
 * 与其他 costsize 函数不同，该例程做出两个实际决策：
 * 执行器是否需要进行标记/恢复，以及我们是否应
 * 材料化内部路径。构建单独路径测试这些替代方案在逻辑上会更清晰，
 * 但那会要求重复大多数成本计算，这并不便宜。
 * 由于选择不会影响输出路径键或启动成本，只有总成本，
 * 因此没有必要保留多个路径。因此，最好在此处做出决策并将其记录在路径的
 * skip_mark_restore 和 materialize_inner 字段中。
 *
 * 标记/恢复的开销通常是必需的，但如果我们知道
 * 执行器只需找到每个外部元组的一个匹配，并且
 * mergeclauses 足以识别匹配，则可以跳过。
 *
 * 如果需要标记/恢复并且内部路径不能支持标记/恢复，或使用一个插入的
 * Material 节点处理标记/恢复更为便宜，我们将材料化内部路径。
 *
 * 'path' 除了行和成本字段以及
 *		skip_mark_restore 和 materialize_inner 已经填好
 * 'workspace' 是来自初始成本合并连接的结果
 * 'extra' 包含有关连接的杂项信息
 */
void final_cost_mergejoin(PlannerInfo *fc_root, MergePath *fc_path,
					 JoinCostWorkspace *fc_workspace,
					 JoinPathExtraData *fc_extra)
{
	Path	   *fc_outer_path = fc_path->jpath.outerjoinpath;
	Path	   *fc_inner_path = fc_path->jpath.innerjoinpath;
	double		fc_inner_path_rows = fc_inner_path->rows;
	List	   *fc_mergeclauses = fc_path->path_mergeclauses;
	List	   *fc_innersortkeys = fc_path->innersortkeys;
	Cost		fc_startup_cost = fc_workspace->startup_cost;
	Cost		fc_run_cost = fc_workspace->run_cost;
	Cost		fc_inner_run_cost = fc_workspace->inner_run_cost;
	double		fc_outer_rows = fc_workspace->outer_rows;
	double		fc_inner_rows = fc_workspace->inner_rows;
	double		fc_outer_skip_rows = fc_workspace->outer_skip_rows;
	double		fc_inner_skip_rows = fc_workspace->inner_skip_rows;
	Cost		fc_cpu_per_tuple,
				fc_bare_inner_cost,
				fc_mat_inner_cost;
	QualCost	fc_merge_qual_cost;
	QualCost	fc_qp_qual_cost;
	double		fc_mergejointuples,
				fc_rescannedtuples;
	double		fc_rescanratio;

	/* 保护下面的假设，即行数不是零 */
	if (fc_inner_path_rows <= 0)
		fc_inner_path_rows = 1;

	/* 使用正确的行估计标记路径 */
	if (fc_path->jpath.path.param_info)
		fc_path->jpath.path.rows = fc_path->jpath.path.param_info->ppi_rows;
	else
		fc_path->jpath.path.rows = fc_path->jpath.path.parent->rows;

	/* 对于部分路径，缩放行估计。 */
	if (fc_path->jpath.path.parallel_workers > 0)
	{
		double		fc_parallel_divisor = get_parallel_divisor(&fc_path->jpath.path);

		fc_path->jpath.path.rows =
			clamp_row_est(fc_path->jpath.path.rows / fc_parallel_divisor);
	}

	/*
	 * 我们可以在初步估计中包括disable_cost，但那
	 * 将等同于优化连接方法禁用的情况，这似乎不是赌注的方向。
	 */
	if (!enable_mergejoin)
		fc_startup_cost += disable_cost;

	/*
	 * 分别计算 mergequals 和 qpquals（其他限制子句）的成本。
	 */
	cost_qual_eval(&fc_merge_qual_cost, fc_mergeclauses, fc_root);
	cost_qual_eval(&fc_qp_qual_cost, fc_path->jpath.joinrestrictinfo, fc_root);
	fc_qp_qual_cost.startup -= fc_merge_qual_cost.startup;
	fc_qp_qual_cost.per_tuple -= fc_merge_qual_cost.per_tuple;

	/*
	 * 对于 SEMI 或 ANTI 连接，或者如果 innerrel 是已知唯一的，执行器
	 * 在第一次匹配后将停止扫描匹配。当所有连接子句都是合并子句时，
	 * 这意味着我们不需要回溯合并，因此可以跳过标记/恢复开销。
	 */
	if ((fc_path->jpath.jointype == JOIN_SEMI ||
		 fc_path->jpath.jointype == JOIN_ANTI ||
		 fc_extra->inner_unique) &&
		(list_length(fc_path->jpath.joinrestrictinfo) ==
		 list_length(fc_path->path_mergeclauses)))
		fc_path->skip_mark_restore = true;
	else
		fc_path->skip_mark_restore = false;

	/*
	 * 获取通过 mergequals 的大致 # 元组。我们在这里使用 approx_tuple_count
	 * 因为我们需要一个使用 JOIN_INNER 语义的估计。
	 */
	fc_mergejointuples = fc_approx_tuple_count(fc_root, &fc_path->jpath, fc_mergeclauses);

	/*
	 * 当外部关系中存在相等的合并键时，合并连接
	 * 必须重新扫描内部关系中的任何匹配元组。这意味着
	 * 重新获取内部元组；我们必须估计这种情况发生的频率。
	 *
	 * 对于常规的内部和外部连接，重新获取的数量可以通过以下方式进行
	 * 大致估计，即合并连接输出的大小减去内部关系的大小。假设不同的键值为 1、2、...
	 * 并将每个键在外部关系中的值数量表示为 m1、m2、...；在内部关系中为 n1、n2、... 
	 * 那么我们有
	 *
	 * 连接的大小 = m1 * n1 + m2 * n2 + ...
	 *
	 * 重新扫描的元组数量 = (m1 - 1) * n1 + (m2 - 1) * n2 + ... = m1 *
	 * n1 + m2 * n2 + ... - (n1 + n2 + ...) = 连接的大小 - 内部
	 * 关系的大小
	 *
	 * 当外部元组没有内部匹配时（nk = 0），此方程是正确的，
	 * 但对于内部元组没有外部匹配（mk = 0）则不然；我们实际上是从重新扫描的元组数量中减去这些，
	 * 实际上是不应该的。我们能否在没有昂贵选择性的情况下做得更好？
	 *
	 * 如果我们从一个唯一化的外部输入工作，或者我们知道根本不需要标记/恢复，
	 * 整个问题就无关紧要了。
	 */
	if (IsA(fc_outer_path, UniquePath) || fc_path->skip_mark_restore)
		fc_rescannedtuples = 0;
	else
	{
		fc_rescannedtuples = fc_mergejointuples - fc_inner_path_rows;
		/* 必须限制，因为可能低估 */
		if (fc_rescannedtuples < 0)
			fc_rescannedtuples = 0;
	}

	/*
	 * 我们将根据重扫描的需要，将各种成本提高到这个程度。注意
	 * 这需要乘以与 inner_rows 相关的某个数，或与我们要扫描的内层关系部分相关的另一个数字。
	 */
	fc_rescanratio = 1.0 + (fc_rescannedtuples / fc_inner_rows);

	/*
	 * 决定我们是否要物化内层输入，以保护其免受标记/恢复和重新获取的影响。我们对常规
	 * 重新获取的成本模型是，重新获取的成本与原始获取相同，
	 * 这可能是过高的估计；但另一方面，我们忽略了标记/恢复的
	 * 记账成本。尚不清楚是否值得开发一个更精细的模型。
	 * 所以我们只需要根据 rescanratio 增加内部运行成本。
	 */
	fc_bare_inner_cost = fc_inner_run_cost * fc_rescanratio;

	/*
	 * 当我们插入一个 Material 节点时，重新获取的成本假定为
	 * 每个元组仅 cpu_operator_cost，与底层
	 * 计划的成本无关；我们还会对每个原始
	 * 获取收取额外的 cpu_operator_cost。注意，我们假设物化节点将
	 * 永远不会溢出到磁盘，因为它只需要记住元组直至
	 * 上一次标记。（如果存在大量重复数据，我们的其他成本
	 * 因素将使这条路径如此昂贵，以至于它可能不会被
	 * 选择。）所以我们在这里不使用 cost_rescan。
	 *
	 * 注意：保持此估计与 create_mergejoin_plan 对生成的
	 * Material 节点的标记保持同步。
	 */
	fc_mat_inner_cost = fc_inner_run_cost +
		cpu_operator_cost * fc_inner_rows * fc_rescanratio;

	/*
	 * 如果我们根本不需要标记/恢复，我们就不需要物化。
	 */
	if (fc_path->skip_mark_restore)
		fc_path->materialize_inner = false;

	/*
	 * 如果看起来物化更便宜，则优先进行物化，除非用户要求抑制
	 * 物化。
	 */
	else if (enable_material && fc_mat_inner_cost < fc_bare_inner_cost)
		fc_path->materialize_inner = true;

	/*
	 * 即使物化看起来并不更便宜，如果内层路径将直接
	 * 使用（不排序），并且不支持标记/恢复，我们 *必须* 进行物化。
	 *
	 * 由于内侧必须有序，并且只有 Sorts 和 IndexScans 可以
	 * 首先创建顺序，并且它们都支持标记/恢复，你可能会认为没有问题——
	 * 但你错了。Nestloop 和 merge 连接可以 *保留* 输入的顺序，
	 * 所以它们可以作为 mergejoin 的输入进行选择，而目前它们
	 * 不支持标记/恢复。
	 *
	 * 我们不在这里测试 enable_material 的值，因为
	 * 在这种情况下，物化是确保正确性所必需的，
	 * 关闭它并不能让我们提供一个无效的计划。
	 */
	else if (fc_innersortkeys == NIL &&
			 !ExecSupportsMarkRestore(fc_inner_path))
		fc_path->materialize_inner = true;

	/*
	 * 此外，如果内层路径要排序且预计将溢出到磁盘，
	 * 则强制执行物化。这是因为最终的合并
	 * 过程可以在不支持标记/恢复的情况下实时完成。
	 * 不过，我们不会尝试调整成本估计以考虑这一因素。
	 *
	 * 由于在这种情况下物化是性能优化，而不是为确保正确性所必需的，
	 * 如果 enable_material 关闭，我们将跳过它。
	 */
	else if (enable_material && fc_innersortkeys != NIL &&
			 fc_relation_byte_size(fc_inner_path_rows,
								fc_inner_path->pathtarget->width) >
			 (work_mem * 1024L))
		fc_path->materialize_inner = true;
	else
		fc_path->materialize_inner = false;

	/* 收取所选情况的正确增量成本 */
	if (fc_path->materialize_inner)
		fc_run_cost += fc_mat_inner_cost;
	else
		fc_run_cost += fc_bare_inner_cost;

	/* CPU成本 */

	/*
	 * 所需的元组比较数量大致是外层行数加上内层行数加上重扫元组数（我们能否
	 * 进一步细化这个？）。在每次比较时，我们需要评估合并连接的条件。
	 */
	fc_startup_cost += fc_merge_qual_cost.startup;
	fc_startup_cost += fc_merge_qual_cost.per_tuple *
		(fc_outer_skip_rows + fc_inner_skip_rows * fc_rescanratio);
	fc_run_cost += fc_merge_qual_cost.per_tuple *
		((fc_outer_rows - fc_outer_skip_rows) +
		 (fc_inner_rows - fc_inner_skip_rows) * fc_rescanratio);

	/*
	 * 对于每个通过合并连接的元组，我们收取
	 * cpu_tuple_cost 加上评估在连接中应用的附加限制
	 * 子句的成本。（这是悲观的，因为并不是所有的条件在每个元组上都会被评估。）
	 *
	 * 注意：我们可以在这里调整 SEIMI/ANTI 连接跳过某些条件
	 * 评估，但这可能不值得麻烦。
	 */
	fc_startup_cost += fc_qp_qual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qp_qual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_mergejointuples;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->jpath.path.pathtarget->cost.startup;
	fc_run_cost += fc_path->jpath.path.pathtarget->cost.per_tuple * fc_path->jpath.path.rows;

	fc_path->jpath.path.startup_cost = fc_startup_cost;
	fc_path->jpath.path.total_cost = fc_startup_cost + fc_run_cost;
}

/*
 * 运行 mergejoinscansel()，带缓存
 */
static MergeScanSelCache *
fc_cached_scansel(PlannerInfo *fc_root, RestrictInfo *fc_rinfo, PathKey *fc_pathkey)
{
	MergeScanSelCache *fc_cache;
	ListCell   *fc_lc;
	Selectivity fc_leftstartsel,
				fc_leftendsel,
				fc_rightstartsel,
				fc_rightendsel;
	MemoryContext fc_oldcontext;

	/* 我们已经有这个结果了吗？ */
	foreach(fc_lc, fc_rinfo->scansel_cache)
	{
		fc_cache = (MergeScanSelCache *) lfirst(fc_lc);
		if (fc_cache->opfamily == fc_pathkey->pk_opfamily &&
			fc_cache->collation == fc_pathkey->pk_eclass->ec_collation &&
			fc_cache->strategy == fc_pathkey->pk_strategy &&
			fc_cache->nulls_first == fc_pathkey->pk_nulls_first)
			return fc_cache;
	}

	/* 不，没有，进行计算 */
	mergejoinscansel(fc_root,
					 (Node *) fc_rinfo->clause,
					 fc_pathkey->pk_opfamily,
					 fc_pathkey->pk_strategy,
					 fc_pathkey->pk_nulls_first,
					 &fc_leftstartsel,
					 &fc_leftendsel,
					 &fc_rightstartsel,
					 &fc_rightendsel);

	/* 将结果缓存到适当持久的工作区 */
	fc_oldcontext = MemoryContextSwitchTo(fc_root->planner_cxt);

	fc_cache = (MergeScanSelCache *) palloc(sizeof(MergeScanSelCache));
	fc_cache->opfamily = fc_pathkey->pk_opfamily;
	fc_cache->collation = fc_pathkey->pk_eclass->ec_collation;
	fc_cache->strategy = fc_pathkey->pk_strategy;
	fc_cache->nulls_first = fc_pathkey->pk_nulls_first;
	fc_cache->leftstartsel = fc_leftstartsel;
	fc_cache->leftendsel = fc_leftendsel;
	fc_cache->rightstartsel = fc_rightstartsel;
	fc_cache->rightendsel = fc_rightendsel;

	fc_rinfo->scansel_cache = lappend(fc_rinfo->scansel_cache, fc_cache);

	MemoryContextSwitchTo(fc_oldcontext);

	return fc_cache;
}

/*
 * initial_cost_hashjoin
 *	  哈希连接路径的初步成本估计。
 *
 * 这必须快速生成路径启动和总成本的下限估算。如果我们无法使用下限排除建议的路径，那么将调用 final_cost_hashjoin 以获得最终估算。
 *
 * 这个函数与 final_cost_hashjoin 之间的确切分工是私有的，代表了初步估算速度与获得紧密下限之间的权衡。我们选择不在此处检查连接条件（除了计算哈希条件的数量），因此我们无法对此做太多。
 * 我们假设 ExecChooseHashTableSize 在这里足够便宜。
 *
 * 'workspace' 将填充启动成本、总成本，以及最终成本哈希连接可能使用的其他数据
 * 'jointype' 是要执行的连接类型
 * 'hashclauses' 是要用作哈希条件的连接条件列表
 * 'outer_path' 是连接的外部输入
 * 'inner_path' 是连接的内部输入
 * 'extra' 包含有关连接的其他信息
 * 'parallel_hash' 表明内路径是部分的，并且将并行构建共享哈希表
 */
void initial_cost_hashjoin(PlannerInfo *fc_root, JoinCostWorkspace *fc_workspace,
					  JoinType fc_jointype,
					  List *fc_hashclauses,
					  Path *fc_outer_path, Path *fc_inner_path,
					  JoinPathExtraData *fc_extra,
					  bool fc_parallel_hash)
{
	Cost		fc_startup_cost = 0;
	Cost		fc_run_cost = 0;
	double		fc_outer_path_rows = fc_outer_path->rows;
	double		fc_inner_path_rows = fc_inner_path->rows;
	double		fc_inner_path_rows_total = fc_inner_path_rows;
	int			fc_num_hashclauses = list_length(fc_hashclauses);
	int			fc_numbuckets;
	int			fc_numbatches;
	int			fc_num_skew_mcvs;
	size_t		fc_space_allowed;	/* 未使用 */

	/* 源数据的成本 */
	fc_startup_cost += fc_outer_path->startup_cost;
	fc_run_cost += fc_outer_path->total_cost - fc_outer_path->startup_cost;
	fc_startup_cost += fc_inner_path->total_cost;

	/*
	 * 计算哈希函数的成本：必须对每个输入元组执行一次。我们
	 * 对每列的哈希函数收取一个 cpu_operator_cost。此外，
	 * 每个内部行收取一个 cpu_tuple_cost，以建模将行插入哈希表的成本。
	 *
	 * XXX 当哈希条件比单个操作符复杂时，我们确实
	 * 应该在此处收取左侧或右侧的额外评估成本。
	 * 这似乎在目前看来工作量大于价值。
	 */
	fc_startup_cost += (cpu_operator_cost * fc_num_hashclauses + cpu_tuple_cost)
		* fc_inner_path_rows;
	fc_run_cost += cpu_operator_cost * fc_num_hashclauses * fc_outer_path_rows;

	/*
	 * 如果这是一个并行哈希构建，则我们当前对此的 inner_rows_total 的值
	 * 仅指各参与者返回的行。为了估算共享哈希表大小，我们需要总数，
	 * 因此需要撤销除法。
	 */
	if (fc_parallel_hash)
		fc_inner_path_rows_total *= get_parallel_divisor(fc_inner_path);

	/*
	 * 获取执行器将用于内部关系的哈希表大小。
	 *
	 * XXX 目前，总是假设将执行偏斜优化。只要 SKEW_HASH_MEM_PERCENT 较小，
	 * 就不值得尝试确定这一点。
	 *
	 * XXX 在某个时候，尝试在成本估计中考虑偏斜优化可能会很有趣，
	 * 但现在，我们不这样做。
	 */
	ExecChooseHashTableSize(fc_inner_path_rows_total,
							fc_inner_path->pathtarget->width,
							true,	/* useskew */
							fc_parallel_hash,	/* 尝试组合哈希内存 */
							fc_outer_path->parallel_workers,
							&fc_space_allowed,
							&fc_numbuckets,
							&fc_numbatches,
							&fc_num_skew_mcvs);

	/*
	 * 如果内部关系太大，则我们需要“批处理”连接，
	 * 这意味着大多数元组将额外一次写入和读取到磁盘。
	 * 对每页收取 seq_page_cost，因为 I/O 应该是顺序的。
	 * 写入内部关系算作启动成本，所有其余的算作运行成本。
	 */
	if (fc_numbatches > 1)
	{
		double		fc_outerpages = fc_page_size(fc_outer_path_rows,
										   fc_outer_path->pathtarget->width);
		double		fc_innerpages = fc_page_size(fc_inner_path_rows,
										   fc_inner_path->pathtarget->width);

		fc_startup_cost += seq_page_cost * fc_innerpages;
		fc_run_cost += seq_page_cost * (fc_innerpages + 2 * fc_outerpages);
	}

	/* CPU成本留待后续处理 */

	/* 公共结果字段 */
	fc_workspace->startup_cost = fc_startup_cost;
	fc_workspace->total_cost = fc_startup_cost + fc_run_cost;
	/* 为 final_cost_hashjoin 保存私有数据 */
	fc_workspace->run_cost = fc_run_cost;
	fc_workspace->numbuckets = fc_numbuckets;
	fc_workspace->numbatches = fc_numbatches;
	fc_workspace->inner_rows_total = fc_inner_path_rows_total;
}

/*
 * final_cost_hashjoin
 *	  哈希连接路径的最终成本和结果大小估计。
 *
 * 注意：num_batches 估计也保存到 'path' 以供后用
 *
 * 'path' 已填充，除了行和成本字段以及
 *		num_batches
 * 'workspace' 是来自 initial_cost_hashjoin 的结果
 * 'extra' 包含有关连接的其他信息
 */
void final_cost_hashjoin(PlannerInfo *fc_root, HashPath *fc_path,
					JoinCostWorkspace *fc_workspace,
					JoinPathExtraData *fc_extra)
{
	Path	   *fc_outer_path = fc_path->jpath.outerjoinpath;
	Path	   *fc_inner_path = fc_path->jpath.innerjoinpath;
	double		fc_outer_path_rows = fc_outer_path->rows;
	double		fc_inner_path_rows = fc_inner_path->rows;
	double		fc_inner_path_rows_total = fc_workspace->inner_rows_total;
	List	   *fc_hashclauses = fc_path->path_hashclauses;
	Cost		fc_startup_cost = fc_workspace->startup_cost;
	Cost		fc_run_cost = fc_workspace->run_cost;
	int			fc_numbuckets = fc_workspace->numbuckets;
	int			fc_numbatches = fc_workspace->numbatches;
	Cost		fc_cpu_per_tuple;
	QualCost	fc_hash_qual_cost;
	QualCost	fc_qp_qual_cost;
	double		fc_hashjointuples;
	double		fc_virtualbuckets;
	Selectivity fc_innerbucketsize;
	Selectivity fc_innermcvfreq;
	ListCell   *fc_hcl;

	/* 使用正确的行估计标记路径 */
	if (fc_path->jpath.path.param_info)
		fc_path->jpath.path.rows = fc_path->jpath.path.param_info->ppi_rows;
	else
		fc_path->jpath.path.rows = fc_path->jpath.path.parent->rows;

	/* 对于部分路径，缩放行估计。 */
	if (fc_path->jpath.path.parallel_workers > 0)
	{
		double		fc_parallel_divisor = get_parallel_divisor(&fc_path->jpath.path);

		fc_path->jpath.path.rows =
			clamp_row_est(fc_path->jpath.path.rows / fc_parallel_divisor);
	}

	/*
	 * 我们可以在初步估计中包括disable_cost，但那
	 * 将等同于优化连接方法禁用的情况，这似乎不是赌注的方向。
	 */
	if (!enable_hashjoin)
		fc_startup_cost += disable_cost;

	/* 用估计的批次数标记路径 */
	fc_path->num_batches = fc_numbatches;

	/* 存储总元组数（部分行估计的总和） */
	fc_path->inner_rows_total = fc_inner_path_rows_total;

	/* 并计算整个连接中的“虚拟”桶的数量 */
	fc_virtualbuckets = (double) fc_numbuckets * (double) fc_numbatches;

	/*
	 * 确定内部关系的桶大小分数和 MCV 频率。
	 * 我们使用为任何单个哈希条件估算的最小桶大小或 MCV 频率；这无疑是保守的。
	 *
	 * 但是：如果内部关系已经唯一化，我们可以假设它适合哈希。
	 * 这很重要，因为这是正确的答案，并且
	 * 因为我们避免用对非唯一化路径错误的值污染缓存。
	 */
	if (IsA(fc_inner_path, UniquePath))
	{
		fc_innerbucketsize = 1.0 / fc_virtualbuckets;
		fc_innermcvfreq = 0.0;
	}
	else
	{
		fc_innerbucketsize = 1.0;
		fc_innermcvfreq = 1.0;
		foreach(fc_hcl, fc_hashclauses)
		{
			RestrictInfo *fc_restrictinfo = lfirst_node(RestrictInfo, fc_hcl);
			Selectivity fc_thisbucketsize;
			Selectivity fc_thismcvfreq;

			/*
			 * 首先，我们必须弄清楚哈希连接条件的哪一侧是内部侧。
			 *
			 * 由于我们在规划大型查询时往往会反复访问相同的条件，
			 * 我们将桶统计估计缓存到 RestrictInfo 节点中，以避免重复查找统计信息。
			 */
			if (bms_is_subset(fc_restrictinfo->right_relids,
							  fc_inner_path->parent->relids))
			{
				/* 右侧为内部 */
				fc_thisbucketsize = fc_restrictinfo->right_bucketsize;
				if (fc_thisbucketsize < 0)
				{
					/* 尚未缓存 */
					estimate_hash_bucket_stats(fc_root,
											   get_rightop(fc_restrictinfo->clause),
											   fc_virtualbuckets,
											   &fc_restrictinfo->right_mcvfreq,
											   &fc_restrictinfo->right_bucketsize);
					fc_thisbucketsize = fc_restrictinfo->right_bucketsize;
				}
				fc_thismcvfreq = fc_restrictinfo->right_mcvfreq;
			}
			else
			{
				Assert(bms_is_subset(fc_restrictinfo->left_relids,
									 fc_inner_path->parent->relids));
				/* 左侧为内部 */
				fc_thisbucketsize = fc_restrictinfo->left_bucketsize;
				if (fc_thisbucketsize < 0)
				{
					/* 尚未缓存 */
					estimate_hash_bucket_stats(fc_root,
											   get_leftop(fc_restrictinfo->clause),
											   fc_virtualbuckets,
											   &fc_restrictinfo->left_mcvfreq,
											   &fc_restrictinfo->left_bucketsize);
					fc_thisbucketsize = fc_restrictinfo->left_bucketsize;
				}
				fc_thismcvfreq = fc_restrictinfo->left_mcvfreq;
			}

			if (fc_innerbucketsize > fc_thisbucketsize)
				fc_innerbucketsize = fc_thisbucketsize;
			if (fc_innermcvfreq > fc_thismcvfreq)
				fc_innermcvfreq = fc_thismcvfreq;
		}
	}

	/*
	 * 如果包含内部 MCV 的桶会超过 hash_mem，我们不想要哈希，
	 * 除非没有其他选择，因此应用 disable_cost。 （执行器通常通过分割批次来应对过度内存使用，
	 * 但显然，它无法以这种方式分隔相等值，因此在这种情况下，它无法将批次大小降到 hash_mem 以下。）
	 */
	if (fc_relation_byte_size(clamp_row_est(fc_inner_path_rows * fc_innermcvfreq),
						   fc_inner_path->pathtarget->width) > get_hash_memory_limit())
		fc_startup_cost += disable_cost;

	
/*
	 * 计算 hashquals 和 qpquals（其他限制子句）的成本
	 * 单独处理。
	 */
	cost_qual_eval(&fc_hash_qual_cost, fc_hashclauses, fc_root);
	cost_qual_eval(&fc_qp_qual_cost, fc_path->jpath.joinrestrictinfo, fc_root);
	fc_qp_qual_cost.startup -= fc_hash_qual_cost.startup;
	fc_qp_qual_cost.per_tuple -= fc_hash_qual_cost.per_tuple;

	/* CPU成本 */

	if (fc_path->jpath.jointype == JOIN_SEMI ||
		fc_path->jpath.jointype == JOIN_ANTI ||
		fc_extra->inner_unique)
	{
		double		fc_outer_matched_rows;
		Selectivity fc_inner_scan_frac;

		/*
		 * 在 SEMI 或 ANTI 连接中，或者如果 innerrel 是已知唯一的，
		 * 执行器将会在第一次匹配后停止。
		 *
		 * 对于至少有一个匹配的 outer-rel 行，我们可以期待
		 * 在桶中的行数的 1/(match_count+1) 的一部分后，
		 * 桶扫描将停止，如果匹配是均匀分布的。由于它们
		 * 可能并不是均匀分布的，我们对该部分应用一个模糊因子
		 * 2.0。（如果我们使用更大的模糊因子，我们将不得不
		 * 将 inner_scan_frac 限制为最多 1.0；但由于
		 * match_count 至少为 1，现在不需要这样的限制。）
		 */
		fc_outer_matched_rows = rint(fc_outer_path_rows * fc_extra->semifactors.outer_match_frac);
		fc_inner_scan_frac = 2.0 / (fc_extra->semifactors.match_count + 1.0);

		fc_startup_cost += fc_hash_qual_cost.startup;
		fc_run_cost += fc_hash_qual_cost.per_tuple * fc_outer_matched_rows *
			clamp_row_est(fc_inner_path_rows * fc_innerbucketsize * fc_inner_scan_frac) * 0.5;

		/*
		 * 对于没有匹配的 outer-rel 行，情况相当不同。
		 * 首先，没有理由假设这些行优先命中人口稠密的桶；
		 * 相反，假设它们与内部分布不相关，因此它们看到的
		 * 平均桶大小是 inner_path_rows / virtualbuckets。其次，
		 * 它们似乎几乎没有准确的 hash-code 匹配，因此桶中的
		 * 很少有元组实际上需要评估 hash quals。我们没有好的
		 * 方法来估计有多少，但目前假设每个桶条目的有效成本
		 * 是可匹配元组的十分之一。
		 */
		fc_run_cost += fc_hash_qual_cost.per_tuple *
			(fc_outer_path_rows - fc_outer_matched_rows) *
			clamp_row_est(fc_inner_path_rows / fc_virtualbuckets) * 0.05;

		/* 获取将通过基本连接的元组数量 */
		if (fc_path->jpath.jointype == JOIN_ANTI)
			fc_hashjointuples = fc_outer_path_rows - fc_outer_matched_rows;
		else
			fc_hashjointuples = fc_outer_matched_rows;
	}
	else
	{
		/*
		 * 所需的元组比较次数是外部元组数量乘以哈希桶中
		 * 通常的元组数量，即内关系大小乘以其桶大小比例。
		 * 在每个比较中，我们需要评估 hashjoin quals。
		 * 但实际上，给每个元组收取全部的资格评估成本是
		 * 悲观的，因为我们仅在哈希值完全匹配时才评估
		 * 资格。缺乏更好的想法，减少成本估算的一半以
		 * 允许这一点。
		 */
		fc_startup_cost += fc_hash_qual_cost.startup;
		fc_run_cost += fc_hash_qual_cost.per_tuple * fc_outer_path_rows *
			clamp_row_est(fc_inner_path_rows * fc_innerbucketsize) * 0.5;

		/*
		 * 获取通过 hashquals 的大致元组数量。我们在这里使用
		 * approx_tuple_count，因为我们需要使用 JOIN_INNER 语义
		 * 进行估计。
		 */
		fc_hashjointuples = fc_approx_tuple_count(fc_root, &fc_path->jpath, fc_hashclauses);
	}

	/*
	 * 对于每个通过有效 hashjoin 的元组，我们收取
	 * cpu_tuple_cost 加上用于评估将在连接中应用的
	 * 额外限制子句的成本。（这是悲观的，因为
	 * 并非所有的资格在每个元组中都会被评估。）
	 */
	fc_startup_cost += fc_qp_qual_cost.startup;
	fc_cpu_per_tuple = cpu_tuple_cost + fc_qp_qual_cost.per_tuple;
	fc_run_cost += fc_cpu_per_tuple * fc_hashjointuples;

	/* tlist评估成本按输出行支付，而不是按扫描的元组计算 */
	fc_startup_cost += fc_path->jpath.path.pathtarget->cost.startup;
	fc_run_cost += fc_path->jpath.path.pathtarget->cost.per_tuple * fc_path->jpath.path.rows;

	fc_path->jpath.path.startup_cost = fc_startup_cost;
	fc_path->jpath.path.total_cost = fc_startup_cost + fc_run_cost;
}


/*
 * cost_subplan
 *		计算子计划（或初始化计划）的成本。
 *
 * 注意：我们可以从根列表中挖掘子计划的计划，但在实践中
 * 所有调用者都已经方便地拥有它，所以我们让他们传递它。
 */
void cost_subplan(PlannerInfo *fc_root, SubPlan *fc_subplan, Plan *fc_plan)
{
	QualCost	fc_sp_cost;

	/* 计算评估测试表达式的成本 */
	cost_qual_eval(&fc_sp_cost,
				   make_ands_implicit((Expr *) fc_subplan->testexpr),
				   fc_root);

	if (fc_subplan->useHashTable)
	{
		/*
		 * 如果我们正在使用哈希表来存储子查询输出，那么
		 * 评估查询的成本是一次性成本。我们为加装哈希表的
		 * 工作每个元组收取一个 cpu_operator_cost。
		 */
		fc_sp_cost.startup += fc_plan->total_cost +
			cpu_operator_cost * fc_plan->plan_rows;

		/*
		 * 每个元组的成本包括计算左侧表达式的成本，加上探测哈希表的成本。我们已经将左侧表达式作为 testexpr 的一部分进行计算，并且还将为每个比较运算符计算一个 cpu_operator_cost。这对探测成本来说可能太低了，但很难做出更好的估计，所以暂时就这样吧。
		 */
	}
	else
	{
		/*
		 * 否则我们将在每次评估时重新扫描子计划输出。我们需要估计我们实际上需要扫描多少输出。注意：此逻辑应与 plan/subselect.c 中 make_subplan() 使用的 tuple_fraction 估计一致。
		 */
		Cost		fc_plan_run_cost = fc_plan->total_cost - fc_plan->startup_cost;

		if (fc_subplan->subLinkType == EXISTS_SUBLINK)
		{
			/* 我们只需要提取 1 个元组；限制以避免零除 */
			fc_sp_cost.per_tuple += fc_plan_run_cost / clamp_row_est(fc_plan->plan_rows);
		}
		else if (fc_subplan->subLinkType == ALL_SUBLINK ||
				 fc_subplan->subLinkType == ANY_SUBLINK)
		{
			/* 假设我们需要 50%的元组 */
			fc_sp_cost.per_tuple += 0.50 * fc_plan_run_cost;
			/* 每个检查的行也收取一个 cpu_operator_cost */
			fc_sp_cost.per_tuple += 0.50 * fc_plan->plan_rows * cpu_operator_cost;
		}
		else
		{
			/* 假设我们需要所有元组 */
			fc_sp_cost.per_tuple += fc_plan_run_cost;
		}

		/*
		 * 同时考虑子计划的启动成本。如果子计划是非相关的或非直接相关的，并且其最上面的节点是一个可以将其输出物化的节点，则假设我们只需要支付其启动成本一次；否则假设每次都需要支付启动成本。
		 */
		if (fc_subplan->parParam == NIL &&
			ExecMaterializesOutput(nodeTag(fc_plan)))
			fc_sp_cost.startup += fc_plan->startup_cost;
		else
			fc_sp_cost.per_tuple += fc_plan->startup_cost;
	}

	fc_subplan->startup_cost = fc_sp_cost.startup;
	fc_subplan->per_call_cost = fc_sp_cost.per_tuple;
}


/*
 * cost_rescan
 *		给定一个完成的路径，估计在第一次扫描后重新扫描的成本。对于某些 Path 类型，重新扫描比原始扫描便宜（如果没有参数改变），这个函数体现了关于这一点的知识。默认情况下返回 Path 中存储的相同成本。（注意，存储在路径中的成本估计始终是第一次扫描的成本。）
 *
 * 该函数当前并不打算建模诸如由于磁盘块缓存导致的重新扫描更便宜的效果；我们关注的是执行者明确缓存结果的计划类型，或者不重新计算启动等。
 */
static void fc_cost_rescan(PlannerInfo *fc_root, Path *fc_path,
			Cost *fc_rescan_startup_cost,	/* 输出参数 */
			Cost *fc_rescan_total_cost)
{
	switch (fc_path->pathtype)
	{
		case T_FunctionScan:

			/*
			 * 当前，nodeFunctionscan.c 在返回任何行之前始终将函数执行完毕，并将结果缓存到 tuplestore 中。因此，函数评估成本全部是启动成本，并且在重新扫描时不再支付。但是，所有运行成本将再次支付。
			 */
			*fc_rescan_startup_cost = 0;
			*fc_rescan_total_cost = fc_path->total_cost - fc_path->startup_cost;
			break;
		case T_HashJoin:

			/*
			 * 如果是单批次连接，我们在重新扫描期间不需要重建哈希表。
			 */
			if (((HashPath *) fc_path)->num_batches == 1)
			{
				/* 启动成本正好是哈希表构建的成本 */
				*fc_rescan_startup_cost = 0;
				*fc_rescan_total_cost = fc_path->total_cost - fc_path->startup_cost;
			}
			else
			{
				/* 否则，没有特殊处理 */
				*fc_rescan_startup_cost = fc_path->startup_cost;
				*fc_rescan_total_cost = fc_path->total_cost;
			}
			break;
		case T_CteScan:
		case T_WorkTableScan:
			{
				/*
				 * 这些计划类型将其最终结果物化到 tuplestore 或 tuplesort 对象中。因此，重新扫描的成本仅为每个元组 cpu_tuple_cost，除非结果大到需要溢出到磁盘。
				 */
				Cost		fc_run_cost = cpu_tuple_cost * fc_path->rows;
				double		fc_nbytes = fc_relation_byte_size(fc_path->rows,
														fc_path->pathtarget->width);
				long		fc_work_mem_bytes = work_mem * 1024L;

				if (fc_nbytes > fc_work_mem_bytes)
				{
					/* 它将溢出，因此计算重新读取成本 */
					double		fc_npages = ceil(fc_nbytes / BLCKSZ);

					fc_run_cost += seq_page_cost * fc_npages;
				}
				*fc_rescan_startup_cost = 0;
				*fc_rescan_total_cost = fc_run_cost;
			}
			break;
		case T_Material:
		case T_Sort:
			{
				/*
				 * 这些计划类型不仅物化其结果，还不实施 qual 过滤或投影。因此，它们比上述的计划类型更便宜重新扫描。我们仅收取每个元组 cpu_operator_cost。（注意：在您更改之前，请确保与 cost_sort 中的运行成本收费保持同步，并查看 cost_material 中的注释。）
				 */
				Cost		fc_run_cost = cpu_operator_cost * fc_path->rows;
				double		fc_nbytes = fc_relation_byte_size(fc_path->rows,
														fc_path->pathtarget->width);
				long		fc_work_mem_bytes = work_mem * 1024L;

				if (fc_nbytes > fc_work_mem_bytes)
				{
					/* 它将溢出，因此计算重新读取成本 */
					double		fc_npages = ceil(fc_nbytes / BLCKSZ);

					fc_run_cost += seq_page_cost * fc_npages;
				}
				*fc_rescan_startup_cost = 0;
				*fc_rescan_total_cost = fc_run_cost;
			}
			break;
		case T_Memoize:
			/* 所有的辛勤工作都由 cost_memoize_rescan 完成 */
			fc_cost_memoize_rescan(fc_root, (MemoizePath *) fc_path,
								fc_rescan_startup_cost, fc_rescan_total_cost);
			break;
		default:
			*fc_rescan_startup_cost = fc_path->startup_cost;
			*fc_rescan_total_cost = fc_path->total_cost;
			break;
	}
}


/*
 * cost_qual_eval
 *		估计评估 WHERE 子句的 CPU 成本。输入可以是隐式与的布尔表达式列表或 RestrictInfo 节点的列表。（后者更受欢迎，因为允许缓存结果。）结果包括一次性（启动）组件和每次评估组件。
 */
void cost_qual_eval(QualCost *fc_cost, List *fc_quals, PlannerInfo *fc_root)
{
	cost_qual_eval_context fc_context;
	ListCell   *fc_l;

	fc_context.root = fc_root;
	fc_context.total.startup = 0;
	fc_context.total.per_tuple = 0;

	/* 我们不对顶部的隐式与收取任何费用…… */

	foreach(fc_l, fc_quals)
	{
		Node	   *fc_qual = (Node *) lfirst(fc_l);

		fc_cost_qual_eval_walker(fc_qual, &fc_context);
	}

	*fc_cost = fc_context.total;
}

/*
 * cost_qual_eval_node
 *		同上，对于单个 RestrictInfo 或表达式。
 */
void cost_qual_eval_node(QualCost *fc_cost, Node *fc_qual, PlannerInfo *fc_root)
{
	cost_qual_eval_context fc_context;

	fc_context.root = fc_root;
	fc_context.total.startup = 0;
	fc_context.total.per_tuple = 0;

	fc_cost_qual_eval_walker(fc_qual, &fc_context);

	*fc_cost = fc_context.total;
}

static bool fc_cost_qual_eval_walker(Node *fc_node, cost_qual_eval_context *fc_context)
{
	if (fc_node == NULL)
		return false;

	/*
	 * RestrictInfo 节点包含一个 eval_cost 字段，供该例程使用，因此无需多次评估 qual 子句的成本。如果子句的成本尚未计算，该字段的启动值将包含 -1。
	 */
	if (IsA(fc_node, RestrictInfo))
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) fc_node;

		if (fc_rinfo->eval_cost.startup < 0)
		{
			cost_qual_eval_context fc_locContext;

			fc_locContext.root = fc_context->root;
			fc_locContext.total.startup = 0;
			fc_locContext.total.per_tuple = 0;

			/*
			 * 对于 OR 子句，递归进入标记的树，以便我们也为包含的 RestrictInfo 设置 eval_cost。
			 */
			if (fc_rinfo->orclause)
				fc_cost_qual_eval_walker((Node *) fc_rinfo->orclause, &fc_locContext);
			else
				fc_cost_qual_eval_walker((Node *) fc_rinfo->clause, &fc_locContext);

			/*
			 * 如果 RestrictInfo 被标记为伪常量，则它只会被测试一次，因此将其成本视为全部启动成本。
			 */
			if (fc_rinfo->pseudoconstant)
			{
				/* 启动期间计算一次执行 */
				fc_locContext.total.startup += fc_locContext.total.per_tuple;
				fc_locContext.total.per_tuple = 0;
			}
			fc_rinfo->eval_cost = fc_locContext.total;
		}
		fc_context->total.startup += fc_rinfo->eval_cost.startup;
		fc_context->total.per_tuple += fc_rinfo->eval_cost.per_tuple;
		/* 不要递归进入子节点 */
		return false;
	}

	
/*
	 * 对于给定树中的每个运算符或函数节点，我们根据pg_proc.procost收取预估的执行成本（记得将其乘以cpu_operator_cost）。
	 *
	 * Vars和Consts的收费为零，布尔运算符（AND，OR，NOT）也一样。虽然简单，但比没有模型要好得多。
	 *
	 * 我们是否应该尝试考虑短路计算AND/OR的可能性？可能*不应该*，因为这会使结果依赖于子句的顺序，而我们无权期望当前的子句顺序就是最终使用的顺序。上面的每个RestrictInfo缓存与尝试重新排序子句的效果也不佳。
	 *
	 * 另一个完全被忽略的问题是，如果某个返回集合的函数在树的顶部以下，则其上方的函数/运算符需要被多次评估。在实际使用中，这种情况很少发生，因此不值得增加所需的复杂性；此外，由于我们对函数的行数估计往往是相当虚假的，因此结果也会非常虚假。
	 */
	if (IsA(fc_node, FuncExpr))
	{
		add_function_cost(fc_context->root, ((FuncExpr *) fc_node)->funcid, fc_node,
						  &fc_context->total);
	}
	else if (IsA(fc_node, OpExpr) ||
			 IsA(fc_node, DistinctExpr) ||
			 IsA(fc_node, NullIfExpr))
	{
		/* 依赖于结构等价性将这些全部视为相同 */
		set_opfuncid((OpExpr *) fc_node);
		add_function_cost(fc_context->root, ((OpExpr *) fc_node)->opfuncid, fc_node,
						  &fc_context->total);
	}
	else if (IsA(fc_node, ScalarArrayOpExpr))
	{
		ScalarArrayOpExpr *fc_saop = (ScalarArrayOpExpr *) fc_node;
		Node	   *fc_arraynode = (Node *) lsecond(fc_saop->args);
		QualCost	fc_sacosts;
		QualCost	fc_hcosts;
		int			fc_estarraylen = estimate_array_length(fc_arraynode);

		set_sa_opfuncid(fc_saop);
		fc_sacosts.startup = fc_sacosts.per_tuple = 0;
		add_function_cost(fc_context->root, fc_saop->opfuncid, NULL,
						  &fc_sacosts);

		if (OidIsValid(fc_saop->hashfuncid))
		{
			/* 处理hashed ScalarArrayOpExpr的成本 */
			fc_hcosts.startup = fc_hcosts.per_tuple = 0;

			add_function_cost(fc_context->root, fc_saop->hashfuncid, NULL, &fc_hcosts);
			fc_context->total.startup += fc_sacosts.startup + fc_hcosts.startup;

			/* 估计构建哈希表的成本。 */
			fc_context->total.startup += fc_estarraylen * fc_hcosts.per_tuple;

			/*
			 * XXX 在构建表时，我们是否应该对sacosts.per_tuple收取一点费用，还是可以假设不会有哈希冲突？
			 */

			/*
			 * 哈希表查找的费用。 收取一次哈希和一次比较的费用。
			 */
			fc_context->total.per_tuple += fc_hcosts.per_tuple + fc_sacosts.per_tuple;
		}
		else
		{
			/*
			 * 估计运算符将在确定答案之前应用于大约一半的数组元素。
			 */
			fc_context->total.startup += fc_sacosts.startup;
			fc_context->total.per_tuple += fc_sacosts.per_tuple *
				estimate_array_length(fc_arraynode) * 0.5;
		}
	}
	else if (IsA(fc_node, Aggref) ||
			 IsA(fc_node, WindowFunc))
	{
		/*
		 * Aggref 和 WindowFunc 节点被视为（并且应该被视为）Vars，
		 * 即在当前模型中零执行成本，因为它们在执行时的行为基本上
		 * 类似于 Vars。 由于同样的原因，我们忽略了输入表达式的成本。
		 * 聚合/窗口函数及其参数的实际执行成本必须被计入
		 * Agg 或 WindowAgg 计划节点的具体成本中。
		 */
		return false;			/* 不要递归到子节点 */
	}
	else if (IsA(fc_node, GroupingFunc))
	{
		/* 将此视为成本 1 */
		fc_context->total.per_tuple += cpu_operator_cost;
		return false;			/* 不要递归到子节点 */
	}
	else if (IsA(fc_node, CoerceViaIO))
	{
		CoerceViaIO *fc_iocoerce = (CoerceViaIO *) fc_node;
		Oid			fc_iofunc;
		Oid			fc_typioparam;
		bool		fc_typisvarlena;

		/* 检查结果类型的输入函数 */
		getTypeInputInfo(fc_iocoerce->resulttype,
						 &fc_iofunc, &fc_typioparam);
		add_function_cost(fc_context->root, fc_iofunc, NULL,
						  &fc_context->total);
		/* 检查输入类型的输出函数 */
		getTypeOutputInfo(exprType((Node *) fc_iocoerce->arg),
						  &fc_iofunc, &fc_typisvarlena);
		add_function_cost(fc_context->root, fc_iofunc, NULL,
						  &fc_context->total);
	}
	else if (IsA(fc_node, ArrayCoerceExpr))
	{
		ArrayCoerceExpr *fc_acoerce = (ArrayCoerceExpr *) fc_node;
		QualCost	fc_perelemcost;

		cost_qual_eval_node(&fc_perelemcost, (Node *) fc_acoerce->elemexpr,
							fc_context->root);
		fc_context->total.startup += fc_perelemcost.startup;
		if (fc_perelemcost.per_tuple > 0)
			fc_context->total.per_tuple += fc_perelemcost.per_tuple *
				estimate_array_length((Node *) fc_acoerce->arg);
	}
	else if (IsA(fc_node, RowCompareExpr))
	{
		/* 保守地假设我们将检查所有列 */
		RowCompareExpr *fc_rcexpr = (RowCompareExpr *) fc_node;
		ListCell   *fc_lc;

		foreach(fc_lc, fc_rcexpr->opnos)
		{
			Oid			fc_opid = lfirst_oid(fc_lc);

			add_function_cost(fc_context->root, get_opcode(fc_opid), NULL,
							  &fc_context->total);
		}
	}
	else if (IsA(fc_node, MinMaxExpr) ||
			 IsA(fc_node, SQLValueFunction) ||
			 IsA(fc_node, XmlExpr) ||
			 IsA(fc_node, CoerceToDomain) ||
			 IsA(fc_node, NextValueExpr))
	{
		/* 将所有这些视为成本 1 */
		fc_context->total.per_tuple += cpu_operator_cost;
	}
	else if (IsA(fc_node, CurrentOfExpr))
	{
		/* 报告高成本以防止选择除 TID 扫描之外的任何东西 */
		fc_context->total.startup += disable_cost;
	}
	else if (IsA(fc_node, SubLink))
	{
		/* 该例程不应应用于未制定计划的表达式 */
		elog(ERROR, "cannot handle unplanned sub-select");
	}
	else if (IsA(fc_node, SubPlan))
	{
		/*
		 * 表达式中的子计划节点通常表示子计划将在每次评估时执行，
		 * 因此需相应收费。 （可以作为 InitPlans 执行的子选择
		 * 已经从表达式中移除。）
		 */
		SubPlan    *fc_subplan = (SubPlan *) fc_node;

		fc_context->total.startup += fc_subplan->startup_cost;
		fc_context->total.per_tuple += fc_subplan->per_call_cost;

		/*
		 * 我们不想递归到 testexpr，因为它已经在 SubPlan 节点的成本中
		 * 被计算过了。 所以我们完成了。
		 */
		return false;
	}
	else if (IsA(fc_node, AlternativeSubPlan))
	{
		/*
		 * 任意使用第一个替代计划来进行成本计算。 （我们当然只应包括一种替代方案，
		 * 并且我们尚未获得足够的信息来知道执行者最有可能使用哪一个。）
		 */
		AlternativeSubPlan *fc_asplan = (AlternativeSubPlan *) fc_node;

		return fc_cost_qual_eval_walker((Node *) linitial(fc_asplan->subplans),
									 fc_context);
	}
	else if (IsA(fc_node, PlaceHolderVar))
	{
		/*
		 * 考虑一般表达式评估成本时，PlaceHolderVar 应被赋予零成本。
		 * 执行包含的表达式的费用作为 tlist 评估成本的一部分，计入
		 * PHV 首次计算的扫描或连接（请参见 set_rel_width 和
		 * add_placeholders_to_joinrel）。 如果我们在这里再次收费，
		 * 我们会对 PHV 涉及的每个层级的成本进行双重计算。
		 * 因此，返回而不递归到 phexpr。
		 */
		return false;
	}

	/* 递归到子节点 */
	return expression_tree_walker(fc_node, fc_cost_qual_eval_walker,
								  (void *) fc_context);
}

/*
 * get_restriction_qual_cost
 *	  计算 baserel 的限制条件的评估成本， plus any
 *	  可移动的连接条件已经被推送到扫描中。
 *	  结果返回到 *qpqual_cost。
 *
 * 这是一个便利的子例程，适用于 seqscans 和其他所有给定的条件
 * 将被暴力评估的情况。 例如，它对于 cost_index() 没有用，
 * 因为索引机制处理一些条件。 我们假设 baserestrictcost
 * 在此之前已通过 set_baserel_size_estimates() 设置。
 */
static void get_restriction_qual_cost(PlannerInfo *fc_root, RelOptInfo *fc_baserel,
						  ParamPathInfo *fc_param_info,
						  QualCost *fc_qpqual_cost)
{
	if (fc_param_info)
	{
		/* 包括被推送下来的子句的费用 */
		cost_qual_eval(fc_qpqual_cost, fc_param_info->ppi_clauses, fc_root);

		fc_qpqual_cost->startup += fc_baserel->baserestrictcost.startup;
		fc_qpqual_cost->per_tuple += fc_baserel->baserestrictcost.per_tuple;
	}
	else
		*fc_qpqual_cost = fc_baserel->baserestrictcost;
}


/*
 * compute_semi_anti_join_factors
 *	  估计 SEMI、ANTI 或 inner_unique 连接可以预期扫描内输入的多少。
 *
 * 在哈希或嵌套循环 SEMI/ANTI 连接中，一旦找到与当前外部行匹配的项，
 * 执行者将停止扫描内部行。 如果我们检测到内部关系是唯一的，
 * 情况也一样。 因此，我们应该为此效应调整一些成本组件。
 * 此函数计算这些调整所需的一些估计。
 * 无论外部和内部关系使用何种特定路径，这些估计都是相同的，
 * 所以我们只需计算一次，然后将其传递给所有连接成本估计函数。
 *
 * 输入参数：
 *	joinrel：考虑中的连接关系
 *	outerrel：考虑中的外部关系
 *	innerrel：考虑中的内部关系
 *	jointype：如果不是 JOIN_SEMI 或 JOIN_ANTI，我们假设它是 inner_unique
 *	sjinfo：与此连接相关的 SpecialJoinInfo
 *	restrictlist：连接条件
 * 输出参数：
 *	*semifactors 被填写（请参见 pathnodes.h 中的字段定义）
 */
void compute_semi_anti_join_factors(PlannerInfo *fc_root,
							   RelOptInfo *fc_joinrel,
							   RelOptInfo *fc_outerrel,
							   RelOptInfo *fc_innerrel,
							   JoinType fc_jointype,
							   SpecialJoinInfo *fc_sjinfo,
							   List *fc_restrictlist,
							   SemiAntiJoinFactors *fc_semifactors)
{
	Selectivity fc_jselec;
	Selectivity fc_nselec;
	Selectivity fc_avgmatch;
	SpecialJoinInfo fc_norm_sjinfo;
	List	   *fc_joinquals;
	ListCell   *fc_l;

	/*
	 * 在 ANTI 连接中，我们必须忽略被“推送下来的”子句，
	 * 因为这些不会影响匹配逻辑。 在 SEMI 连接中，
	 * 我们不区分连接条件与“推送下来的”条件，因此只需使用整个
	 * restrictinfo 列表。 对于其他外连接类型，我们应该仅考虑
	 * 非推送下来的条件，以便这样就可以降级到 IS_OUTER_JOIN 检查。
	 */
	if (IS_OUTER_JOIN(fc_jointype))
	{
		fc_joinquals = NIL;
		foreach(fc_l, fc_restrictlist)
		{
			RestrictInfo *fc_rinfo = lfirst_node(RestrictInfo, fc_l);

			if (!RINFO_IS_PUSHED_DOWN(fc_rinfo, fc_joinrel->relids))
				fc_joinquals = lappend(fc_joinquals, fc_rinfo);
		}
	}
	else
		fc_joinquals = fc_restrictlist;

	/*
	 * 获取连接子句的 JOIN_SEMI 或 JOIN_ANTI 选择性。
	 */
	fc_jselec = clauselist_selectivity(fc_root,
									fc_joinquals,
									0,
									(fc_jointype == JOIN_ANTI) ? JOIN_ANTI : JOIN_SEMI,
									fc_sjinfo);

	/*
	 * 还获取连接子句的普通内部连接选择性。
	 */
	fc_norm_sjinfo.type = T_SpecialJoinInfo;
	fc_norm_sjinfo.min_lefthand = fc_outerrel->relids;
	fc_norm_sjinfo.min_righthand = fc_innerrel->relids;
	fc_norm_sjinfo.syn_lefthand = fc_outerrel->relids;
	fc_norm_sjinfo.syn_righthand = fc_innerrel->relids;
	fc_norm_sjinfo.jointype = JOIN_INNER;
	/* 我们不打算尝试使剩余字段有效 */
	fc_norm_sjinfo.lhs_strict = false;
	fc_norm_sjinfo.delay_upper_joins = false;
	fc_norm_sjinfo.semi_can_btree = false;
	fc_norm_sjinfo.semi_can_hash = false;
	fc_norm_sjinfo.semi_operators = NIL;
	fc_norm_sjinfo.semi_rhs_exprs = NIL;

	fc_nselec = clauselist_selectivity(fc_root,
									fc_joinquals,
									0,
									JOIN_INNER,
									&fc_norm_sjinfo);

	
/* 避免泄漏大量的 ListCells */
	if (IS_OUTER_JOIN(fc_jointype))
		list_free(fc_joinquals);

	/*
	 * jselec 可以解释为外部关系中具有任何匹配的行的比例（这对于 SEMI 和 ANTI 情况都是如此）。
	 * 而 nselec 是与之匹配的笛卡尔积的比例。因此，具有至少一个匹配的每个外部关系行的平均匹配数为
	 * nselec * inner_rows / jselec。
	 *
	 * 注意：在这里使用内部关系的“行”计数是正确的，即使我们可能稍后考虑一个行数更少的参数化内部路径。
	 * 这是因为我们已经将所有的连接条件包括在选择性估计中。
	 */
	if (fc_jselec > 0)				/* 防止除以零 */
	{
		fc_avgmatch = fc_nselec * fc_innerrel->rows / fc_jselec;
		/* 限制在合理范围内 */
		fc_avgmatch = Max(1.0, fc_avgmatch);
	}
	else
		fc_avgmatch = 1.0;

	fc_semifactors->outer_match_frac = fc_jselec;
	fc_semifactors->match_count = fc_avgmatch;
}

/*
 * has_indexed_join_quals
 *	  检查嵌套循环连接的所有连接条件是否用于内部索引条件。
 *
 * 如果 SEMI/ANTI 连接的内部路径是一个索引扫描（包括位图索引扫描），并且使用所有的连接条件作为索引条件，
 * 我们可以假设未匹配的外部元组处理成本低，而在其他情况下，很可能成本较高。
 */
static bool fc_has_indexed_join_quals(NestPath *fc_path)
{
	JoinPath   *fc_joinpath = &fc_path->jpath;
	Relids		fc_joinrelids = fc_joinpath->path.parent->relids;
	Path	   *fc_innerpath = fc_joinpath->innerjoinpath;
	List	   *fc_indexclauses;
	bool		fc_found_one;
	ListCell   *fc_lc;

	/* 如果连接仍有条件需要评估，则不快 */
	if (fc_joinpath->joinrestrictinfo != NIL)
		return false;
	/* 如果内部路径根本没有参数化也不快 */
	if (fc_innerpath->param_info == NULL)
		return false;

	/* 查找内部扫描的 indexclauses 列表 */
	switch (fc_innerpath->pathtype)
	{
		case T_IndexScan:
		case T_IndexOnlyScan:
			fc_indexclauses = ((IndexPath *) fc_innerpath)->indexclauses;
			break;
		case T_BitmapHeapScan:
			{
				/* 仅接受简单的位图扫描，而不是 AND/OR 情况 */
				Path	   *fc_bmqual = ((BitmapHeapPath *) fc_innerpath)->bitmapqual;

				if (IsA(fc_bmqual, IndexPath))
					fc_indexclauses = ((IndexPath *) fc_bmqual)->indexclauses;
				else
					return false;
				break;
			}
		default:

			/* 
			 * 如果它不是一个简单的 indexscan，它可能不会快速
			 * 处理零行输出，即使它是一个使用所有的
			 * joinquals 的参数化路径。
			 */
			return false;
	}

	/* 
	 * 检查内部路径的参数子句。任何来自外部
	 * 路径的子句必须在 indexclauses 列表中找到，要么完全相同
	 * ，要么以 equivclass.c 生成的等效形式存在。我们还必须至少找到
	 * 一个这样的子句，否则它就是一个缺少子句的连接，这并不快。
	 */
	fc_found_one = false;
	foreach(fc_lc, fc_innerpath->param_info->ppi_clauses)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

		if (join_clause_is_movable_into(fc_rinfo,
										fc_innerpath->parent->relids,
										fc_joinrelids))
		{
			if (!is_redundant_with_indexclauses(fc_rinfo, fc_indexclauses))
				return false;
			fc_found_one = true;
		}
	}
	return fc_found_one;
}


/* 
 * approx_tuple_count
 *		对通过一组质询条件的连接行数进行简单估算。
 *
 * 这些质询可以是隐式 AND 的布尔表达式列表，
 * 或者是 RestrictInfo 节点的列表（通常是后者）。
 *
 * 我们故意在 JOIN_INNER 规则下计算选择性，即使
 * 这是一种外连接。这是合适的，因为我们希望
 * 了解有多少元组通过初始合并或哈希
 * 连接步骤。
 *
 * 这是一个粗略估算，因为我们跳过 clauselist_selectivity，
 * 仅将独立子句的选择性相乘。如今
 * clauselist_selectivity 通常无法做到更好，但在某些情况下（例如范围约束）更聪明。
 * 不过，我们不能有效缓存 clauselist_selectivity 的结果，而
 * 单个子句选择性可以并且会被缓存。
 *
 * 由于我们仅使用结果来估算生成并通过 qpqual 检查的潜在
 * 输出元组的数量，因此接受这种近似似乎是可以的。
 */
static double fc_approx_tuple_count(PlannerInfo *fc_root, JoinPath *fc_path, List *fc_quals)
{
	double		fc_tuples;
	double		fc_outer_tuples = fc_path->outerjoinpath->rows;
	double		fc_inner_tuples = fc_path->innerjoinpath->rows;
	SpecialJoinInfo fc_sjinfo;
	Selectivity fc_selec = 1.0;
	ListCell   *fc_l;

	/* 
	 * 为 JOIN_INNER 语义构建一个 SpecialJoinInfo。
	 */
	fc_sjinfo.type = T_SpecialJoinInfo;
	fc_sjinfo.min_lefthand = fc_path->outerjoinpath->parent->relids;
	fc_sjinfo.min_righthand = fc_path->innerjoinpath->parent->relids;
	fc_sjinfo.syn_lefthand = fc_path->outerjoinpath->parent->relids;
	fc_sjinfo.syn_righthand = fc_path->innerjoinpath->parent->relids;
	fc_sjinfo.jointype = JOIN_INNER;
	/* 我们不打算尝试使剩余字段有效 */
	fc_sjinfo.lhs_strict = false;
	fc_sjinfo.delay_upper_joins = false;
	fc_sjinfo.semi_can_btree = false;
	fc_sjinfo.semi_can_hash = false;
	fc_sjinfo.semi_operators = NIL;
	fc_sjinfo.semi_rhs_exprs = NIL;

	/* 获取近似选择性 */
	foreach(fc_l, fc_quals)
	{
		Node	   *fc_qual = (Node *) lfirst(fc_l);

		/* 请注意 clause_selectivity 将能够缓存其结果 */
		fc_selec *= clause_selectivity(fc_root, fc_qual, 0, JOIN_INNER, &fc_sjinfo);
	}

	/* 将其应用于输入关系大小 */
	fc_tuples = fc_selec * fc_outer_tuples * fc_inner_tuples;

	return clamp_row_est(fc_tuples);
}


/* 
 * set_baserel_size_estimates
 *		为给定的基本关系设置大小估算。
 *
 * rel 的 targetlist 和 restrictinfo 列表必须已经构造
 * 完成，且 rel->tuples 必须已设置。
 *
 * 我们设置 rel 节点的以下字段：
 *	rows：输出元组的估计数量（在应用
 *		  约束子句后）。
 *	width：以字节为单位的估计平均输出元组宽度。
 *	baserestrictcost：评估 baserestrictinfo 子句的估计成本。
 */
void set_baserel_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	double		fc_nrows;

	/* 应仅应用于基础关系 */
	Assert(fc_rel->relid > 0);

	fc_nrows = fc_rel->tuples *
		clauselist_selectivity(fc_root,
							   fc_rel->baserestrictinfo,
							   0,
							   JOIN_INNER,
							   NULL);

	fc_rel->rows = clamp_row_est(fc_nrows);

	cost_qual_eval(&fc_rel->baserestrictcost, fc_rel->baserestrictinfo, fc_root);

	set_rel_width(fc_root, fc_rel);
}

/* 
 * get_parameterized_baserel_size
 *		对基本关系的参数化扫描进行大小估算。
 *
 * 'param_clauses' 列出了要使用的额外连接子句。
 *
 * set_baserel_size_estimates 必须已经应用。
 */
double get_parameterized_baserel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							   List *fc_param_clauses)
{
	List	   *fc_allclauses;
	double		fc_nrows;

	/* 
	 * 估算通过参数化扫描返回的行数，知道
	 * 它将应用所有额外的连接子句以及 rel 的自身
	 * 限制子句。请注意，我们在选择性估算期间强制将子句视为
	 * 非连接子句。
	 */
	fc_allclauses = list_concat_copy(fc_param_clauses, fc_rel->baserestrictinfo);
	fc_nrows = fc_rel->tuples *
		clauselist_selectivity(fc_root,
							   fc_allclauses,
							   fc_rel->relid,	/* 不要使用 0！ */
							   JOIN_INNER,
							   NULL);
	fc_nrows = clamp_row_est(fc_nrows);
	/* 为了安全，确保结果不超过基本估计 */
	if (fc_nrows > fc_rel->rows)
		fc_nrows = fc_rel->rows;
	return fc_nrows;
}

/* 
 * set_joinrel_size_estimates
 *		为给定的连接关系设置大小估算。
 *
 * rel 的 targetlist 必须已经构造完毕，并且必须提供一个
 * 与给定组件关系匹配的限制子句列表。
 *
 * 由于为两个以上基本关系创建连接关系的方式有不止一种，
 * 因此我们在此获得的结果可能取决于提供的哪个组件
 * 关系对。从理论上讲，不论提供哪个对，我们应获得相同的答案；
 * 在实践中，由于选择性估算
 * 例程并未均等处理所有情况，因此可能不会。但对此无能为力。
 * （对遇到的每对输入关系重复计算是否有意义，并以某种方式
 * 平均结果？可能会更麻烦，不值得，而且
 * 无论如何我们必须保持统计行数估算对所有路径相同。）
 *
 * 我们仅在此设置 rows 字段。reltarget 字段已经由
 * build_joinrel_tlist 设置，baserestrictcost 对连接关系不使用。
 */
void set_joinrel_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel,
						   RelOptInfo *fc_outer_rel,
						   RelOptInfo *fc_inner_rel,
						   SpecialJoinInfo *fc_sjinfo,
						   List *fc_restrictlist)
{
	fc_rel->rows = fc_calc_joinrel_size_estimate(fc_root,
										   fc_rel,
										   fc_outer_rel,
										   fc_inner_rel,
										   fc_outer_rel->rows,
										   fc_inner_rel->rows,
										   fc_sjinfo,
										   fc_restrictlist);
}

/* 
 * get_parameterized_joinrel_size
 *		对连接关系的参数化扫描进行大小估算。
 *
 * 'rel' 是待考虑的连接关系。
 * 'outer_path'，'inner_path' 是（可能也是参数化的）Paths，产生
 * 被连接的关系。
 * 'sjinfo' 是与此连接相关的任何 SpecialJoinInfo。
 * 'restrict_clauses' 列出了在连接节点需要应用的连接子句
 * （包括任何已移入此连接的可移动子句，不包括任何已推入到
 * 子路径中的可移动子句）。
 *
 * set_joinrel_size_estimates 必须已经应用。
 */
double get_parameterized_joinrel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							   Path *fc_outer_path,
							   Path *fc_inner_path,
							   SpecialJoinInfo *fc_sjinfo,
							   List *fc_restrict_clauses)
{
	double		fc_nrows;

	
/*
	 * 估计通过参数化连接返回的行数，作为输入路径的大小乘以
	 * 到达此连接节点的子句的选择性。
	 *
	 * 与 set_joinrel_size_estimates 一样，行计数估计可能依赖于提供的
	 * 输入路径对，尽管理想情况下，对任何具有相同参数化的路径对
	 * 我们都会得到相同的估计。
	 */
	fc_nrows = fc_calc_joinrel_size_estimate(fc_root,
									   fc_rel,
									   fc_outer_path->parent,
									   fc_inner_path->parent,
									   fc_outer_path->rows,
									   fc_inner_path->rows,
									   fc_sjinfo,
									   fc_restrict_clauses);
	/* 为了安全，确保结果不超过基本估计 */
	if (fc_nrows > fc_rel->rows)
		fc_nrows = fc_rel->rows;
	return fc_nrows;
}

/*
 * calc_joinrel_size_estimate
 *		用于 set_joinrel_size_estimates 和
 *		get_parameterized_joinrel_size 的主要工具。
 *
 * outer_rel/inner_rel 是正在连接的关系，但它们应该被假设为
 * 拥有大小 outer_rows/inner_rows；当我们考虑参数化路径时，这些
 * 数字可能小于 rel->rows 所表示的数字。
 */
static double fc_calc_joinrel_size_estimate(PlannerInfo *fc_root,
						   RelOptInfo *fc_joinrel,
						   RelOptInfo *fc_outer_rel,
						   RelOptInfo *fc_inner_rel,
						   double fc_outer_rows,
						   double fc_inner_rows,
						   SpecialJoinInfo *fc_sjinfo,
						   List *fc_restrictlist_in)
{
	/* 这个看似无用的变量规避了 VS2013 中的编译器错误： */
	List	   *fc_restrictlist = fc_restrictlist_in;
	JoinType	fc_jointype = fc_sjinfo->jointype;
	Selectivity fc_fkselec;
	Selectivity fc_jselec;
	Selectivity fc_pselec;
	double		fc_nrows;

	/*
	 * 计算连接子句选择性。注意，我们仅考虑在此连接层次成为
	 * 限制子句的子句；我们没有重复计算它们，因为在估计组件关系大小时
	 * 并未考虑这些子句。
	 *
	 * 首先，查看是否有任何连接子句可以匹配已知的外键约束。如果是，则
	 * 从 restrictlist 中删除这些子句，而是使用外键语义估计它们的选择性。
	 * （我们这样做不考虑这些子句是本地的还是“下推”的。可能，一个
	 * 匹配外键的子句在外连接时永远不会被视为下推，因为它将是严格的，
	 * 因此将成为连接强度降低的理由。）fkselec 获取与外键匹配子句的
	 * 净选择性，如果没有，则为 1.0。
	 */
	fc_fkselec = get_foreign_key_join_selectivity(fc_root,
											   fc_outer_rel->relids,
											   fc_inner_rel->relids,
											   fc_sjinfo,
											   &fc_restrictlist);

	/*
	 * 对于外连接，我们必须将连接自身的子句（JOIN/ON 条件）的选择性
	 * 与任何被“下推”的子句区分开。对于内连接，我们将其全部视为
	 * 连接子句进行计数。
	 */
	if (IS_OUTER_JOIN(fc_jointype))
	{
		List	   *fc_joinquals = NIL;
		List	   *fc_pushedquals = NIL;
		ListCell   *fc_l;

		/* 遍历子句以分成两个列表 */
		foreach(fc_l, fc_restrictlist)
		{
			RestrictInfo *fc_rinfo = lfirst_node(RestrictInfo, fc_l);

			if (RINFO_IS_PUSHED_DOWN(fc_rinfo, fc_joinrel->relids))
				fc_pushedquals = lappend(fc_pushedquals, fc_rinfo);
			else
				fc_joinquals = lappend(fc_joinquals, fc_rinfo);
		}

		/* 获取单独的选择性 */
		fc_jselec = clauselist_selectivity(fc_root,
										fc_joinquals,
										0,
										fc_jointype,
										fc_sjinfo);
		fc_pselec = clauselist_selectivity(fc_root,
										fc_pushedquals,
										0,
										fc_jointype,
										fc_sjinfo);

		
/* 避免泄漏大量的 ListCells */
		list_free(fc_joinquals);
		list_free(fc_pushedquals);
	}
	else
	{
		fc_jselec = clauselist_selectivity(fc_root,
										fc_restrictlist,
										0,
										fc_jointype,
										fc_sjinfo);
		fc_pselec = 0.0;			/* 未使用，保持编译器安静 */
	}

	/*
	 * 基本上，我们将笛卡尔积的大小乘以选择性。
	 *
	 * 如果我们正在进行外连接，则需要考虑这一点：连接条件的选择性
	 * 必须根据输出必须至少与不可为空输入相同的知识进行限制。
	 * 然而，任何下推的条件将在外连接之后应用，因此它们的选择性
	 * 完全适用。
	 *
	 * 对于 JOIN_SEMI 和 JOIN_ANTI，选择性定义为 LHS 行中有匹配项的
	 * 比例，我们直接应用该比例。
	 */
	switch (fc_jointype)
	{
		case JOIN_INNER:
			fc_nrows = fc_outer_rows * fc_inner_rows * fc_fkselec * fc_jselec;
			/* pselec 未使用 */
			break;
		case JOIN_LEFT:
			fc_nrows = fc_outer_rows * fc_inner_rows * fc_fkselec * fc_jselec;
			if (fc_nrows < fc_outer_rows)
				fc_nrows = fc_outer_rows;
			fc_nrows *= fc_pselec;
			break;
		case JOIN_FULL:
			fc_nrows = fc_outer_rows * fc_inner_rows * fc_fkselec * fc_jselec;
			if (fc_nrows < fc_outer_rows)
				fc_nrows = fc_outer_rows;
			if (fc_nrows < fc_inner_rows)
				fc_nrows = fc_inner_rows;
			fc_nrows *= fc_pselec;
			break;
		case JOIN_SEMI:
			fc_nrows = fc_outer_rows * fc_fkselec * fc_jselec;
			/* pselec 未使用 */
			break;
		case JOIN_ANTI:
			fc_nrows = fc_outer_rows * (1.0 - fc_fkselec * fc_jselec);
			fc_nrows *= fc_pselec;
			break;
		default:
			/* 此处不期望其他值 */
			elog(ERROR, "unrecognized join type: %d", (int) fc_jointype);
			fc_nrows = 0;			/* 保持编译器安静 */
			break;
	}

	return clamp_row_est(fc_nrows);
}

/*
 * get_foreign_key_join_selectivity
 *		估计与外键相关的子句的连接选择性。
 *
 * 从 *restrictlist 中删除任何可以与外键约束匹配的子句，并返回它们的
 * 选择性的替代估计。当没有此类子句时，返回 1.0。
 *
 * 特别处理这些子句的原因是，这样我们可以得到比依赖于
 * clauselist_selectivity() 更好的估计，尤其是对于多列外键，当
 * 该函数假设子句是独立时，这种假设的有效性下降严重。但即使是
 * 单列外键，当 pg_statistic 统计信息缺失或过时时，我们也许能
 * 得到更好的答案。
 */
static Selectivity get_foreign_key_join_selectivity(PlannerInfo *fc_root,
								 Relids fc_outer_relids,
								 Relids fc_inner_relids,
								 SpecialJoinInfo *fc_sjinfo,
								 List **fc_restrictlist)
{
	Selectivity fc_fkselec = 1.0;
	JoinType	fc_jointype = fc_sjinfo->jointype;
	List	   *fc_worklist = *fc_restrictlist;
	ListCell   *fc_lc;

	/* 考虑每个已知与查询匹配的外键约束 */
	foreach(fc_lc, fc_root->fkey_list)
	{
		ForeignKeyOptInfo *fc_fkinfo = (ForeignKeyOptInfo *) lfirst(fc_lc);
		bool		fc_ref_is_outer;
		List	   *fc_removedlist;
		ListCell   *fc_cell;

		/*
		 * 这个外键只有在将一个基础关系连接到另一侧的基础关系时才相关。
		 */
		if (bms_is_member(fc_fkinfo->con_relid, fc_outer_relids) &&
			bms_is_member(fc_fkinfo->ref_relid, fc_inner_relids))
			fc_ref_is_outer = false;
		else if (bms_is_member(fc_fkinfo->ref_relid, fc_outer_relids) &&
				 bms_is_member(fc_fkinfo->con_relid, fc_inner_relids))
			fc_ref_is_outer = true;
		else
			continue;

		/*
		 * 如果我们处理的是半连接/反连接，并且外键所引用的
		 * 关系在外侧，那么对外键的知识并不能帮助我们弄清楚
		 * 我们需要知道什么（即外部行中有匹配的行的比例）。另一方面，
		 * 如果所引用的关系在内部，那么所有外部行必须在
		 * 所引用的表中有匹配（忽略空值）。但是，任何过滤该表的
		 * 限制或连接子句都会降低匹配的比例。我们可以考虑限制
		 * 子句，但猜测有多少行会通过内部的连接太难了。
		 * 因此，如果适用任何一种情况，就放弃并忽略这个外键。
		 */
		if ((fc_jointype == JOIN_SEMI || fc_jointype == JOIN_ANTI) &&
			(fc_ref_is_outer || bms_membership(fc_inner_relids) != BMS_SINGLETON))
			continue;

		/*
		 * 通过删除匹配外键的子句来修改限制列表（并将其放入
		 * removedlist中）。修改最初传入的列表结构似乎不安全，
		 * 所以第一次通过时我们进行浅拷贝。
		 */
		if (fc_worklist == *fc_restrictlist)
			fc_worklist = list_copy(fc_worklist);

		fc_removedlist = NIL;
		foreach(fc_cell, fc_worklist)
		{
			RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_cell);
			bool		fc_remove_it = false;
			int			fc_i;

			/* 如果它匹配外键的任何列，则删除该子句 */
			for (fc_i = 0; fc_i < fc_fkinfo->nkeys; fc_i++)
			{
				if (fc_rinfo->parent_ec)
				{
					/*
					 * EC派生的子句只能通过EC匹配。考虑任何来自于相同
					 * EC的子句作为匹配外键是可以的：即使equivclass.c
					 * 选择生成了一个等于其他变量组合的子句，它也可以
					 * 生成一个等于外键变量的子句。因此，为了估算的
					 * 目的，我们可以把它当做这样做了。
					 *
					 * 注意：检查parent_ec有点作弊，因为
					 * 存在EC派生子句没有设置parent_ec；但这样的
					 * 子句必须比较的表达式不仅仅是变量，因此它们无论如何
					 * 不能匹配外键。
					 */
					if (fc_fkinfo->eclass[fc_i] == fc_rinfo->parent_ec)
					{
						fc_remove_it = true;
						break;
					}
				}
				else
				{
					/*
					 * 否则，看看rinfo是否之前作为“宽松”子句匹配到了外键。
					 */
					if (list_member_ptr(fc_fkinfo->rinfos[fc_i], fc_rinfo))
					{
						fc_remove_it = true;
						break;
					}
				}
			}
			if (fc_remove_it)
			{
				fc_worklist = foreach_delete_current(fc_worklist, fc_cell);
				fc_removedlist = lappend(fc_removedlist, fc_rinfo);
			}
		}

		/*
		 * 如果我们未能删除预期找到的所有匹配子句，就放弃
		 * 并忽略这个外键；应用它的选择性可能导致重复计数。
		 * 将我们确实能够删除的子句放回工作列表中。
		 *
		 * 由于匹配子句已知不是外连接延迟的，它们通常会出现在
		 * 初始连接子句列表中。如果我们没有找到它们，有两种可能性：
		 *
		 * 1. 如果外键匹配基于一个EC，并且该EC的ec_has_const，
		 * 它根本不会生成任何连接子句。我们在检查是否有“全部”
		 * 子句时会排除这样的EC。（在下面，我们将调整
		 * 选择性估算以应对这种情况。）
		 *
		 * 2. 这些子句在此循环的前一次迭代中与其他外键匹配，
		 * 因此从工作列表中删除。（一种可能的情况是，两个
		 * 外键匹配到相同的EC；在初始列表中只会有一个EC派生子句，
		 * 因此第一个外键会消耗掉它。）独立应用两个外键的选择性
		 * 有可能低估连接大小；特别是，这将推翻EC发明的主要目的之一，
		 * 即避免对冗余等式条件选择性的重复计数。
		 * 后续我们可能会想到合理的方法来组合估计，但现在，
		 * 只需放弃，因为这是一种相对不常见的情况。
		 */
		if (fc_removedlist == NIL ||
			list_length(fc_removedlist) !=
			(fc_fkinfo->nmatched_ec - fc_fkinfo->nconst_ec + fc_fkinfo->nmatched_ri))
		{
			fc_worklist = list_concat(fc_worklist, fc_removedlist);
			continue;
		}

		/*
		 * 最后我们得到了回报：利用知识来估算选择性，即
		 * 每个引用行将恰好匹配所引用表中的一行。
		 *
		 * XXX 在引用列中存在空值的情况下，这不成立，
		 * 因此原则上我们应该对这些进行降级估计。
		 * 然而（1）如果在查询其他地方对于引用列有严格的
		 * 限制子句，则在这里降级将导致空值部分的重复计数，
		 * 并且（2）如何组合多个引用列的空值部分也不是很清楚。
		 * 因此，我们现在不对空值进行任何修正。
		 *
		 * XXX 另一个要点是如果外键约束的一端是继承父表，
		 * 我们估算好像约束覆盖它的所有子表。这对于引用表
		 * 不是不合理的假设，即用户可能对所有子表应用了
		 * 相同的约束（尽管我们应该检查这一点）。但对于
		 * 所引用的表而言，无法做到这一点。幸运的是，
		 * 正是因为这一点行不通，实际上用外键引用父表
		 * 的情况并不常见。因此，至少现在，在这里不考虑
		 * 继承。
		 */
		if (fc_jointype == JOIN_SEMI || fc_jointype == JOIN_ANTI)
		{
			
/*
			 * 对于 JOIN_SEMI 和 JOIN_ANTI，我们只有在 FK 引用的表恰好是连接的内部时才会到这里。连接的选择性定义为左侧表（LHS）行与匹配行的比例。FK 意味着每个 LHS 行在 *引用表* 中都有一个匹配；但是对它的任何限制条件都会减少匹配的数量。因此，我们将连接选择性视为等于表限制条件的选择性，即行数 / 元组；但我们必须防范元组 == 0。
			 */
			RelOptInfo *fc_ref_rel = find_base_rel(fc_root, fc_fkinfo->ref_relid);
			double		fc_ref_tuples = Max(fc_ref_rel->tuples, 1.0);

			fc_fkselec *= fc_ref_rel->rows / fc_ref_tuples;
		}
		else
		{
			/*
			 * 否则，选择性恰好为 1 / 引用表大小；但是要防范元组 == 0。请注意，我们应该使用原始表元组计数，而不是其过滤或连接大小的任何估计。
			 */
			RelOptInfo *fc_ref_rel = find_base_rel(fc_root, fc_fkinfo->ref_relid);
			double		fc_ref_tuples = Max(fc_ref_rel->tuples, 1.0);

			fc_fkselec *= 1.0 / fc_ref_tuples;
		}

		/*
		 * 如果任何 FK 列参与了 ec_has_const EC，那么 equivclass.c 将为连接的每一侧生成 “var = const” 限制，从而减少两个输入关系的大小。将 fkselec 视为表面意义上的选择性将导致对引用 Var 的常量限制的选择性双重计数。因此，查找施加在引用 Var（们）上的限制条件，并除去它们的选择性以进行更正。
		 */
		if (fc_fkinfo->nconst_ec > 0)
		{
			for (int fc_i = 0; fc_i < fc_fkinfo->nkeys; fc_i++)
			{
				EquivalenceClass *fc_ec = fc_fkinfo->eclass[fc_i];

				if (fc_ec && fc_ec->ec_has_const)
				{
					EquivalenceMember *fc_em = fc_fkinfo->fk_eclass_member[fc_i];
					RestrictInfo *fc_rinfo = find_derived_clause_for_ec_member(fc_ec,
																			fc_em);

					if (fc_rinfo)
					{
						Selectivity fc_s0;

						fc_s0 = clause_selectivity(fc_root,
												(Node *) fc_rinfo,
												0,
												fc_jointype,
												fc_sjinfo);
						if (fc_s0 > 0)
							fc_fkselec /= fc_s0;
					}
				}
			}
		}
	}

	*fc_restrictlist = fc_worklist;
	CLAMP_PROBABILITY(fc_fkselec);
	return fc_fkselec;
}

/*
 * set_subquery_size_estimates
 *		设置作为子查询的基础关系的大小估计。
 *
 * 该关系的目标列表和限制信息列表必须已经构造完成，且子查询的路径必须已经完成。
 * 我们查看子查询的 PlannerInfo 以提取数据。
 *
 * 我们设置与 set_baserel_size_estimates 相同的字段。
 */
void set_subquery_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	PlannerInfo *fc_subroot = fc_rel->subroot;
	RelOptInfo *fc_sub_final_rel;
	ListCell   *fc_lc;

	/* 只应适用于作为子查询的基础关系 */
	Assert(fc_rel->relid > 0);
	Assert(planner_rt_fetch(fc_rel->relid, fc_root)->rtekind == RTE_SUBQUERY);

	/*
	 * 从子查询复制输出行的原始数量。它的所有路径应该具有相同的输出行计数，因此只需查看 cheapest-total。
	 */
	fc_sub_final_rel = fetch_upper_rel(fc_subroot, UPPERREL_FINAL, NULL);
	fc_rel->tuples = fc_sub_final_rel->cheapest_total_path->rows;

	/*
	 * 通过检查子查询的目标列表计算每个输出列的宽度估计。对于任何作为普通 Var 的输出，获取在规划子查询时进行的宽度估计。否则，我们将其留给 set_rel_width 来填充基于数据类型的默认估计。
	 */
	foreach(fc_lc, fc_subroot->parse->targetList)
	{
		TargetEntry *fc_te = lfirst_node(TargetEntry, fc_lc);
		Node	   *fc_texpr = (Node *) fc_te->expr;
		int32		fc_item_width = 0;

		/* 垃圾列在上层查询中不可见 */
		if (fc_te->resjunk)
			continue;

		/*
		 * 子查询可能是一个视图的扩展，自当前查询解析后添加了列，因此其中有非垃圾的 tlist 列并且与我们查询级别上可见的任何列不对应。忽略这样的列。
		 */
		if (fc_te->resno < fc_rel->min_attr || fc_te->resno > fc_rel->max_attr)
			continue;

		/*
		 * XXX 当前这不适用于包含集合操作的子查询，因为它们的 tlist 中的 Vars 是对第一个叶子子查询的虚假引用，即使我们仍然能够访问其 PlannerInfo，也不会给出正确的答案。
		 *
		 * 此外，子查询可能是一个 appendrel，所有分支由于约束排除已知为空，在这种情况下 set_append_rel_pathlist 将 attr_widths 设置为零。
		 *
		 * 在任何情况下，我们只是将宽度估计留为零，直到 set_rel_width 修复它。
		 */
		if (IsA(fc_texpr, Var) &&
			fc_subroot->parse->setOperations == NULL)
		{
			Var		   *fc_var = (Var *) fc_texpr;
			RelOptInfo *fc_subrel = find_base_rel(fc_subroot, fc_var->varno);

			fc_item_width = fc_subrel->attr_widths[fc_var->varattno - fc_subrel->min_attr];
		}
		fc_rel->attr_widths[fc_te->resno - fc_rel->min_attr] = fc_item_width;
	}

	/* 现在估计输出行数量等 */
	set_baserel_size_estimates(fc_root, fc_rel);
}

/*
 * set_function_size_estimates
 *		设置作为函数调用的基础关系的大小估计。
 *
 * 该关系的目标列表和限制信息列表必须已经构造完成。
 *
 * 我们设置与 set_baserel_size_estimates 相同的字段。
 */
void set_function_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	RangeTblEntry *fc_rte;
	ListCell   *fc_lc;

	/* 只应适用于作为函数的基础关系 */
	Assert(fc_rel->relid > 0);
	fc_rte = planner_rt_fetch(fc_rel->relid, fc_root);
	Assert(fc_rte->rtekind == RTE_FUNCTION);

	/*
	 * 估计函数将返回的行数。节点的行计数为最大函数结果的行数。
	 */
	fc_rel->tuples = 0;
	foreach(fc_lc, fc_rte->functions)
	{
		RangeTblFunction *fc_rtfunc = (RangeTblFunction *) lfirst(fc_lc);
		double		fc_ntup = expression_returns_set_rows(fc_root, fc_rtfunc->funcexpr);

		if (fc_ntup > fc_rel->tuples)
			fc_rel->tuples = fc_ntup;
	}

	/* 现在估计输出行数量等 */
	set_baserel_size_estimates(fc_root, fc_rel);
}

/*
 * set_function_size_estimates
 *		设置作为函数调用的基础关系的大小估计。
 *
 * 该关系的目标列表和限制信息列表必须已经构造完成。
 *
 * 我们设置与 set_tablefunc_size_estimates 相同的字段。
 */
void set_tablefunc_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	/* 只应适用于作为函数的基础关系 */
	Assert(fc_rel->relid > 0);
	Assert(planner_rt_fetch(fc_rel->relid, fc_root)->rtekind == RTE_TABLEFUNC);

	fc_rel->tuples = 100;

	/* 现在估计输出行数量等 */
	set_baserel_size_estimates(fc_root, fc_rel);
}

/*
 * set_values_size_estimates
 *		设置作为值列表的基础关系的大小估计。
 *
 * 该关系的目标列表和限制信息列表必须已经构造完成。
 *
 * 我们设置与 set_baserel_size_estimates 相同的字段。
 */
void set_values_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	RangeTblEntry *fc_rte;

	/* 只应适用于作为值列表的基础关系 */
	Assert(fc_rel->relid > 0);
	fc_rte = planner_rt_fetch(fc_rel->relid, fc_root);
	Assert(fc_rte->rtekind == RTE_VALUES);

	/*
	 * 估计值列表将返回的行数。我们根据列表长度精确知道这一点（当然，排除列表项中的集合返回函数，但这在其他地方也没有处理）。
	 */
	fc_rel->tuples = list_length(fc_rte->values_lists);

	/* 现在估计输出行数量等 */
	set_baserel_size_estimates(fc_root, fc_rel);
}

/*
 * set_cte_size_estimates
 *		设置作为 CTE 引用的基础关系的大小估计。
 *
 * 该关系的目标列表和限制信息列表必须已经构造完成，我们需要对 CTE 返回的行数进行估计（如果是常规 CTE）或非递归项（如果是自引用）。
 *
 * 我们设置与 set_baserel_size_estimates 相同的字段。
 */
void set_cte_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel, double fc_cte_rows)
{
	RangeTblEntry *fc_rte;

	/* 仅应应用于作为 CTE 引用的基础关系 */
	Assert(fc_rel->relid > 0);
	fc_rte = planner_rt_fetch(fc_rel->relid, fc_root);
	Assert(fc_rte->rtekind == RTE_CTE);

	if (fc_rte->self_reference)
	{
		
/*
		 * 在自引用中，我们假设平均工作表大小是非递归项大小的倍数。最佳倍数将根据查询的“分支因子”而有所不同，因此其值应可调节。
		 */
		fc_rel->tuples = clamp_row_est(recursive_worktable_factor * fc_cte_rows);
	}
	else
	{
		/* 否则只需相信CTE的行数估算 */
		fc_rel->tuples = fc_cte_rows;
	}

	/* 现在估计输出行数量等 */
	set_baserel_size_estimates(fc_root, fc_rel);
}

/*
 * set_namedtuplestore_size_estimates
 *		设置作为tuplestore引用的基础关系的大小估算。
 *
 * rel的目标列表和限制信息列表必须已经构建。
 *
 * 我们设置与set_baserel_size_estimates相同的字段。
 */
void set_namedtuplestore_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	RangeTblEntry *fc_rte;

	/* 应仅应用于作为tuplestore引用的基础关系 */
	Assert(fc_rel->relid > 0);
	fc_rte = planner_rt_fetch(fc_rel->relid, fc_root);
	Assert(fc_rte->rtekind == RTE_NAMEDTUPLESTORE);

	/*
	 * 使用生成命名的 tuplestore 的代码提供的估计值。在某些情况下，实际数量可能是可用的；在其他情况下，将重用相同的计划，因此可能会估计并使用一个“典型”值。
	 */
	fc_rel->tuples = fc_rte->enrtuples;
	if (fc_rel->tuples < 0)
		fc_rel->tuples = 1000;

	/* 现在估计输出行数量等 */
	set_baserel_size_estimates(fc_root, fc_rel);
}

/*
 * set_result_size_estimates
 *	设置 RTE_RESULT 基关系的大小估计
 *
 * 关系的目标列表和限制信息列表必须已经构建。
 *
 * 我们设置与 set_baserel_size_estimates 相同的字段。
 */
void set_result_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	/* 只应适用于 RTE_RESULT 基础关系 */
	Assert(fc_rel->relid > 0);
	Assert(planner_rt_fetch(fc_rel->relid, fc_root)->rtekind == RTE_RESULT);

	/* RTE_RESULT 总是生成一行，原生 */
	fc_rel->tuples = 1;

	/* 现在估计输出行数量等 */
	set_baserel_size_estimates(fc_root, fc_rel);
}

/*
 * set_foreign_size_estimates
 *	设置外部表的基关系的大小估计。
 *
 * 在这里我们能做的事情不多；外部数据包装器负责生成有用的估计。我们可以不错地估计 baserestrictcost，因此我们设置了它，同时也根据目标列表中的类型驱动估计设置宽度。对行值没有办法做出合理的处理，因此我们只是放一个默认估计，希望包装器能对此进行改进。包装器的 GetForeignRelSize 函数将会稍后被调用。
 *
 * 关系的目标列表和限制信息列表必须已经构建。
 */
void set_foreign_size_estimates(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	/* 应仅应用于基础关系 */
	Assert(fc_rel->relid > 0);

	fc_rel->rows = 1000;			/* 完全虚假的默认估计 */

	cost_qual_eval(&fc_rel->baserestrictcost, fc_rel->baserestrictinfo, fc_root);

	set_rel_width(fc_root, fc_rel);
}


/*
 * set_rel_width
 *	设置基关系的估计输出宽度。
 *
 * 估计输出宽度是对实际上被引用的列的每个属性宽度估计之和，加上在此关系中必须计算的任何 PHV 或其他表达式。这是我们需要在排序、哈希等情况下向上传递的数据量。
 *
 * 此函数还设置 reltarget->cost，因此现在有点命名不当。
 *
 * 注意：这在普通关系上效果最佳，因为它倾向于查看真实的 Vars。对于子查询，set_subquery_size_estimates 已经复制了子查询内进行的每列估计，对于其他类型的关系，我们也无能为力。如果无法获得更好的数字，我们会退回到（相当愚蠢的）基于数据类型的宽度估计。
 *
 * 每个属性的宽度估计在构建连接关系或扫描/连接 pathtarget 时被缓存以便重复使用。
 */
static void set_rel_width(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	Oid			fc_reloid = planner_rt_fetch(fc_rel->relid, fc_root)->relid;
	int32		fc_tuple_width = 0;
	bool		fc_have_wholerow_var = false;
	ListCell   *fc_lc;

	/* Vars 被假定为费用为零，但其他表达式不是 */
	fc_rel->reltarget->cost.startup = 0;
	fc_rel->reltarget->cost.per_tuple = 0;

	foreach(fc_lc, fc_rel->reltarget->exprs)
	{
		Node	   *fc_node = (Node *) lfirst(fc_lc);

		/*
		 * 通常，关系的目标列表中的 Var 必须属于该关系；但是在涉及 LATERAL 引用的边缘情况下并非如此。如果 Var 的 varno 错误，则回退到通用情况（似乎没有理由更聪明）。
		 */
		if (IsA(fc_node, Var) &&
			((Var *) fc_node)->varno == fc_rel->relid)
		{
			Var		   *fc_var = (Var *) fc_node;
			int			fc_ndx;
			int32		fc_item_width;

			Assert(fc_var->varattno >= fc_rel->min_attr);
			Assert(fc_var->varattno <= fc_rel->max_attr);

			fc_ndx = fc_var->varattno - fc_rel->min_attr;

			/*
			 * 如果它是一个整体行 Var，我们将在下面处理它，因为我们已经缓存了尽可能多的属性宽度。
			 */
			if (fc_var->varattno == 0)
			{
				fc_have_wholerow_var = true;
				continue;
			}

			/*
			 * 宽度可能已经被缓存（特别是如果它是一个子查询），因此不要重复努力。
			 */
			if (fc_rel->attr_widths[fc_ndx] > 0)
			{
				fc_tuple_width += fc_rel->attr_widths[fc_ndx];
				continue;
			}

			/* 尝试从统计数据中获取列宽 */
			if (fc_reloid != InvalidOid && fc_var->varattno > 0)
			{
				fc_item_width = get_attavgwidth(fc_reloid, fc_var->varattno);
				if (fc_item_width > 0)
				{
					fc_rel->attr_widths[fc_ndx] = fc_item_width;
					fc_tuple_width += fc_item_width;
					continue;
				}
			}

			/*
			 * 不是普通关系，或者无法找到其统计数据。仅使用类型信息进行估计。
			 */
			fc_item_width = get_typavgwidth(fc_var->vartype, fc_var->vartypmod);
			Assert(fc_item_width > 0);
			fc_rel->attr_widths[fc_ndx] = fc_item_width;
			fc_tuple_width += fc_item_width;
		}
		else if (IsA(fc_node, PlaceHolderVar))
		{
			/*
			 * 在扫描此关系时，我们需要评估 PHV 包含的表达式，因此一定要将其包含在 reltarget->cost 中。
			 */
			PlaceHolderVar *fc_phv = (PlaceHolderVar *) fc_node;
			PlaceHolderInfo *fc_phinfo = find_placeholder_info(fc_root, fc_phv, false);
			QualCost	fc_cost;

			fc_tuple_width += fc_phinfo->ph_width;
			cost_qual_eval_node(&fc_cost, (Node *) fc_phv->phexpr, fc_root);
			fc_rel->reltarget->cost.startup += fc_cost.startup;
			fc_rel->reltarget->cost.per_tuple += fc_cost.per_tuple;
		}
		else
		{
			/*
			 * 我们可能正在查看从子查询中提取的表达式，或者一个代表整体行子 Var 的 ROW()，等等。根据表达式类型信息做我们能做的事情。
			 */
			int32		fc_item_width;
			QualCost	fc_cost;

			fc_item_width = get_typavgwidth(exprType(fc_node), exprTypmod(fc_node));
			Assert(fc_item_width > 0);
			fc_tuple_width += fc_item_width;
			/* 并不完全清楚我们是否需要考虑成本，但还是这样做 */
			cost_qual_eval_node(&fc_cost, fc_node, fc_root);
			fc_rel->reltarget->cost.startup += fc_cost.startup;
			fc_rel->reltarget->cost.per_tuple += fc_cost.per_tuple;
		}
	}

	/*
	 * 如果我们有一个整体行引用，估计其宽度为每列宽度之和加上堆元组头的开销。
	 */
	if (fc_have_wholerow_var)
	{
		int32		fc_wholerow_width = MAXALIGN(SizeofHeapTupleHeader);

		if (fc_reloid != InvalidOid)
		{
			/* 真实关系，因此估计真实元组宽度 */
			fc_wholerow_width += get_relation_data_width(fc_reloid,
													  fc_rel->attr_widths - fc_rel->min_attr);
		}
		else
		{
			/* 根据关于虚假关系的信息做我们能做的事情 */
			AttrNumber	fc_i;

			for (fc_i = 1; fc_i <= fc_rel->max_attr; fc_i++)
				fc_wholerow_width += fc_rel->attr_widths[fc_i - fc_rel->min_attr];
		}

		fc_rel->attr_widths[0 - fc_rel->min_attr] = fc_wholerow_width;

		/*
		 * 将整体行 Var 包含在输出元组中。是的，这确实是在运行时发生的事情。
		 */
		fc_tuple_width += fc_wholerow_width;
	}

	Assert(fc_tuple_width >= 0);
	fc_rel->reltarget->width = fc_tuple_width;
}

/*
 * set_pathtarget_cost_width
 *	设置 PathTarget tlist 的估计评估成本和输出宽度。
 *
 * 作为一种记号方便，返回传入的相同 PathTarget 指针。
 *
 * 大多数（但并非全部）使用此函数的情况发生在我们已经为基关系运行 set_rel_width() 之后；因此我们通常可以获取缓存的 Vars 宽度估计。如果无法获取，则回退到基于数据类型的宽度估计。现有的早期规划中对 PathTargets 的使用并不需要准确的宽度到足以值得去目录中查找更好的数据。
 */
PathTarget * set_pathtarget_cost_width(PlannerInfo *fc_root, PathTarget *fc_target)
{
	int32		fc_tuple_width = 0;
	ListCell   *fc_lc;

	/* Vars 被假定为费用为零，但其他表达式不是 */
	fc_target->cost.startup = 0;
	fc_target->cost.per_tuple = 0;

	foreach(fc_lc, fc_target->exprs)
	{
		Node	   *fc_node = (Node *) lfirst(fc_lc);

		if (IsA(fc_node, Var))
		{
			Var		   *fc_var = (Var *) fc_node;
			int32		fc_item_width;

			/* 我们不应该在这里看到任何上层的 Vars */
			Assert(fc_var->varlevelsup == 0);

			/* 尝试从 RelOptInfo 缓存中获取数据 */
			if (!IS_SPECIAL_VARNO(fc_var->varno) &&
				fc_var->varno < fc_root->simple_rel_array_size)
			{
				RelOptInfo *fc_rel = fc_root->simple_rel_array[fc_var->varno];

				if (fc_rel != NULL &&
					fc_var->varattno >= fc_rel->min_attr &&
					fc_var->varattno <= fc_rel->max_attr)
				{
					int			fc_ndx = fc_var->varattno - fc_rel->min_attr;

					if (fc_rel->attr_widths[fc_ndx] > 0)
					{
						fc_tuple_width += fc_rel->attr_widths[fc_ndx];
						continue;
					}
				}
			}

			/*
			 * 没有可用缓存数据，因此仅使用类型信息进行估计。
			 */
			fc_item_width = get_typavgwidth(fc_var->vartype, fc_var->vartypmod);
			Assert(fc_item_width > 0);
			fc_tuple_width += fc_item_width;
		}
		else
		{
			/*
			 * 使用类型信息处理一般表达式。
			 */
			int32		fc_item_width;
			QualCost	fc_cost;

			fc_item_width = get_typavgwidth(exprType(fc_node), exprTypmod(fc_node));
			Assert(fc_item_width > 0);
			fc_tuple_width += fc_item_width;

			/* 也要考虑成本 */
			cost_qual_eval_node(&fc_cost, fc_node, fc_root);
			fc_target->cost.startup += fc_cost.startup;
			fc_target->cost.per_tuple += fc_cost.per_tuple;
		}
	}

	Assert(fc_tuple_width >= 0);
	fc_target->width = fc_tuple_width;

	return fc_target;
}

/*
 * relation_byte_size
 *	  估算给定宽度（字节大小）的给定数量的元组的存储空间（字节）。
 */
static double fc_relation_byte_size(double fc_tuples, int fc_width)
{
	return fc_tuples * (MAXALIGN(fc_width) + MAXALIGN(SizeofHeapTupleHeader));
}



/*
 * page_size
 *	  返回给定宽度（以字节为单位）的给定数量的元组所覆盖页面数量的估计值。
 */
static double fc_page_size(double fc_tuples, int fc_width)
{
	return ceil(fc_relation_byte_size(fc_tuples, fc_width) / BLCKSZ);
}

/*
 * 估计每个工作者在路径上预算的工作者数量中将完成的工作量的比例。
 */
static double get_parallel_divisor(Path *fc_path)
{
	double		fc_parallel_divisor = fc_path->parallel_workers;

	/*
	 * 早期的并行查询经验表明，当只有一个工作者时，领导者通常对执行计划的并行部分做出非常重要的贡献，但随着更多工作者的加入，它的贡献越来越少，因为它忙于从工作者那里读取元组并进行任何所需的非并行后处理。当我们达到 4 个工作者时，领导者不再做出有意义的贡献。因此，目前估计领导者花费 30% 的时间为每个工作者提供服务，其余时间执行并行计划。
	 */
	if (parallel_leader_participation)
	{
		double		fc_leader_contribution;

		fc_leader_contribution = 1.0 - (0.3 * fc_path->parallel_workers);
		if (fc_leader_contribution > 0)
			fc_parallel_divisor += fc_leader_contribution;
	}

	return fc_parallel_divisor;
}

/*
 * compute_bitmap_pages
 *
 * 计算从堆中获取的页面数量，以用于位图堆扫描。
 */
double compute_bitmap_pages(PlannerInfo *fc_root, RelOptInfo *fc_baserel, Path *fc_bitmapqual,
					 int fc_loop_count, Cost *fc_cost, double *fc_tuple)
{
	Cost		fc_indexTotalCost;
	Selectivity fc_indexSelectivity;
	double		fc_T;
	double		fc_pages_fetched;
	double		fc_tuples_fetched;
	double		fc_heap_pages;
	long		fc_maxentries;

	/*
	 * 获取获取位图的总成本，以及它的总选择性。
	 */
	cost_bitmap_tree_node(fc_bitmapqual, &fc_indexTotalCost, &fc_indexSelectivity);

	/*
	 * 估算获取的主表页面数量。
	 */
	fc_tuples_fetched = clamp_row_est(fc_indexSelectivity * fc_baserel->tuples);

	fc_T = (fc_baserel->pages > 1) ? (double) fc_baserel->pages : 1.0;

	/*
	 * 对于单次扫描，需要获取的堆页面数量与Mackert和Lohman公式在T <= b的情况下相同（即不需要重新读取）。
	 */
	fc_pages_fetched = (2.0 * fc_T * fc_tuples_fetched) / (2.0 * fc_T + fc_tuples_fetched);

	/*
	 * 计算从堆中获取的页面数量。然后根据当前的work_mem估算获取位图中的最大条目数量。
	 * （请注意，我们总是根据在单次迭代中获取的页面数量进行此计算，即使loop_count > 1。
	 * 这是正确的，因为只有该数量的条目会同时存储在位图中。）
	 */
	fc_heap_pages = Min(fc_pages_fetched, fc_baserel->pages);
	fc_maxentries = tbm_calculate_entries(work_mem * 1024L);

	if (fc_loop_count > 1)
	{
		/*
		 * 对于重复的位图扫描，将Mackert和Lohman公式中获取的元组数量按扫描次数进行缩放，以便我们
		 * 估算所有扫描获取的页面数量。然后按一次扫描进行比例分配。
		 */
		fc_pages_fetched = index_pages_fetched(fc_tuples_fetched * fc_loop_count,
											fc_baserel->pages,
											get_indexpath_pages(fc_bitmapqual),
											fc_root);
		fc_pages_fetched /= fc_loop_count;
	}

	if (fc_pages_fetched >= fc_T)
		fc_pages_fetched = fc_T;
	else
		fc_pages_fetched = ceil(fc_pages_fetched);

	if (fc_maxentries < fc_heap_pages)
	{
		double		fc_exact_pages;
		double		fc_lossy_pages;

		/*
		 * 对损失页面数量的粗略估算。由于tbm_lossify()的编码方式，损失页面的数量在内存不足时急剧增加；
		 * 这个公式具有这种特性，并且在测试中似乎表现得足够好，但我们有可能以某种方式做得更好。
		 */
		fc_lossy_pages = Max(0, fc_heap_pages - fc_maxentries / 2);
		fc_exact_pages = fc_heap_pages - fc_lossy_pages;

		/*
		 * 如果存在损失页面，则重新计算由位图堆节点处理的元组数量。我们假设给定元组来自精确页面的概率
		 * 与给定页面是精确的概率相同。这可能不符合实际，但不清楚我们能否做得更好。
		 */
		if (fc_lossy_pages > 0)
			fc_tuples_fetched =
				clamp_row_est(fc_indexSelectivity *
							  (fc_exact_pages / fc_heap_pages) * fc_baserel->tuples +
							  (fc_lossy_pages / fc_heap_pages) * fc_baserel->tuples);
	}

	if (fc_cost)
		*fc_cost = fc_indexTotalCost;
	if (fc_tuple)
		*fc_tuple = fc_tuples_fetched;

	return fc_pages_fetched;
}
